diff --git a/java/pom.xml b/java/pom.xml
index cf8216bd..8f737749 100644
--- a/java/pom.xml
+++ b/java/pom.xml
@@ -44,6 +44,7 @@
4.3.0
9.4.1212
1.10.7
+ 1.0.34
3.8.1
2.22.2
@@ -90,6 +91,11 @@
yaks-testing-http
${project.version}
+
+ dev.yaks
+ yaks-testing-swagger
+ ${project.version}
+
dev.yaks
yaks-testing-standard
@@ -161,6 +167,7 @@
yaks-testing-camel-k
yaks-testing-http
yaks-testing-jdbc
+ yaks-testing-swagger
yaks-testing-standard
diff --git a/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpClientSteps.java b/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpClientSteps.java
index 05fb0de7..af0b9217 100644
--- a/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpClientSteps.java
+++ b/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpClientSteps.java
@@ -22,6 +22,7 @@
import com.consol.citrus.exceptions.CitrusRuntimeException;
import com.consol.citrus.http.client.HttpClient;
import com.consol.citrus.http.message.HttpMessage;
+import com.consol.citrus.variable.dictionary.DataDictionary;
import cucumber.api.Scenario;
import cucumber.api.java.Before;
import cucumber.api.java.en.Given;
@@ -33,15 +34,15 @@
import org.apache.http.conn.ssl.TrustAllStrategy;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.ssl.SSLContexts;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.util.StringUtils;
/**
* @author Christoph Deppisch
*/
-public class HttpClientSteps {
+public class HttpClientSteps implements HttpSteps {
@CitrusResource
private TestRunner runner;
@@ -53,17 +54,21 @@ public class HttpClientSteps {
private String requestUrl;
- private HttpMessage request;
- private HttpMessage response;
-
private Map requestHeaders = new HashMap<>();
private Map responseHeaders = new HashMap<>();
+ private Map requestParams = new HashMap<>();
private Map bodyValidationExpressions = new HashMap<>();
+ private String requestMessageType;
+ private String responseMessageType;
+
private String requestBody;
private String responseBody;
+ private DataDictionary outboundDictionary;
+ private DataDictionary inboundDictionary;
+
@Before
public void before(Scenario scenario) {
if (httpClient == null && citrus.getApplicationContext().getBeansOfType(HttpClient.class).size() == 1L) {
@@ -76,11 +81,14 @@ public void before(Scenario scenario) {
requestHeaders = new HashMap<>();
responseHeaders = new HashMap<>();
- request = new HttpMessage();
- response = new HttpMessage();
+ requestParams = new HashMap<>();
+ requestMessageType = Citrus.DEFAULT_MESSAGE_TYPE;
+ responseMessageType = Citrus.DEFAULT_MESSAGE_TYPE;
requestBody = null;
responseBody = null;
bodyValidationExpressions = new HashMap<>();
+ outboundDictionary = null;
+ inboundDictionary = null;
}
@Given("^http-client \"([^\"\\s]+)\"$")
@@ -108,6 +116,10 @@ public void setUrl(String url) {
@Then("^(?:expect|verify) HTTP response header ([^\\s]+)(?:=| is )\"(.+)\"$")
public void addResponseHeader(String name, String value) {
+ if (name.equals(HttpHeaders.CONTENT_TYPE)) {
+ responseMessageType = getMessageType(value);
+ }
+
responseHeaders.put(name, value);
}
@@ -119,9 +131,18 @@ public void addResponseHeaders(DataTable headers) {
@Given("^HTTP request header ([^\\s]+)(?:=| is )\"(.+)\"$")
public void addRequestHeader(String name, String value) {
+ if (name.equals(HttpHeaders.CONTENT_TYPE)) {
+ requestMessageType = getMessageType(value);
+ }
+
requestHeaders.put(name, value);
}
+ @Given("^HTTP request query parameter ([^\\s]+)(?:=| is )\"(.+)\"$")
+ public void addRequestQueryParam(String name, String value) {
+ requestParams.put(name, value);
+ }
+
@Given("^HTTP request headers$")
public void addRequestHeaders(DataTable headers) {
Map headerPairs = headers.asMap(String.class, String.class);
@@ -176,41 +197,15 @@ public void sendClientRequestMultilineBody(String method) {
@When("^send (GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) ([^\"\\s]+)$")
public void sendClientRequest(String method, String path) {
- request.method(HttpMethod.valueOf(method));
-
- if (StringUtils.hasText(path)) {
- request.path(path);
- request.contextPath(path);
- }
-
- if (StringUtils.hasText(requestBody)) {
- request.setPayload(requestBody);
- }
-
- for (Map.Entry headerEntry : requestHeaders.entrySet()) {
- request.setHeader(headerEntry.getKey(), headerEntry.getValue());
- }
-
- sendClientRequest(request);
-
+ sendClientRequest(createRequest(requestBody, requestHeaders, requestParams, method, path));
requestBody = null;
requestHeaders.clear();
+ requestParams.clear();
}
@Then("^receive HTTP (\\d+)(?: [^\\s]+)?$")
public void receiveClientResponse(Integer status) {
- response.status(HttpStatus.valueOf(status));
-
- if (StringUtils.hasText(responseBody)) {
- response.setPayload(responseBody);
- }
-
- for (Map.Entry headerEntry : responseHeaders.entrySet()) {
- response.setHeader(headerEntry.getKey(), headerEntry.getValue());
- }
-
- receiveClientResponse(response);
-
+ receiveClientResponse(createResponse(responseBody, responseHeaders, status));
responseBody = null;
responseHeaders.clear();
}
@@ -247,6 +242,12 @@ private void sendClientRequest(HttpMessage request) {
if (StringUtils.hasText(requestUrl)) {
requestBuilder.uri(requestUrl);
}
+
+ requestBuilder.messageType(requestMessageType);
+
+ if (outboundDictionary != null) {
+ requestBuilder.dictionary(outboundDictionary);
+ }
};
runner.http(action);
@@ -266,6 +267,12 @@ private void receiveClientResponse(HttpMessage response) {
responseBuilder.validate(headerEntry.getKey(), headerEntry.getValue());
}
bodyValidationExpressions.clear();
+
+ responseBuilder.messageType(responseMessageType);
+
+ if (inboundDictionary != null) {
+ responseBuilder.dictionary(inboundDictionary);
+ }
});
}
@@ -300,4 +307,22 @@ private org.apache.http.client.HttpClient sslClient() {
throw new CitrusRuntimeException("Failed to create http client for ssl connection", e);
}
}
+
+ /**
+ * Specifies the inboundDictionary.
+ *
+ * @param inboundDictionary
+ */
+ public void setInboundDictionary(DataDictionary inboundDictionary) {
+ this.inboundDictionary = inboundDictionary;
+ }
+
+ /**
+ * Specifies the outboundDictionary.
+ *
+ * @param outboundDictionary
+ */
+ public void setOutboundDictionary(DataDictionary outboundDictionary) {
+ this.outboundDictionary = outboundDictionary;
+ }
}
diff --git a/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpServerSteps.java b/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpServerSteps.java
index bec1656b..01325a82 100644
--- a/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpServerSteps.java
+++ b/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpServerSteps.java
@@ -21,14 +21,13 @@
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import io.cucumber.datatable.DataTable;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
-import org.springframework.http.HttpStatus;
-import org.springframework.util.StringUtils;
/**
* @author Christoph Deppisch
*/
-public class HttpServerSteps {
+public class HttpServerSteps implements HttpSteps {
@CitrusResource
private TestRunner runner;
@@ -38,14 +37,15 @@ public class HttpServerSteps {
private HttpServer httpServer;
- private HttpMessage request;
- private HttpMessage response;
-
private Map requestHeaders = new HashMap<>();
private Map responseHeaders = new HashMap<>();
+ private Map requestParams = new HashMap<>();
private Map bodyValidationExpressions = new HashMap<>();
+ private String requestMessageType;
+ private String responseMessageType;
+
private String requestBody;
private String responseBody;
@@ -61,8 +61,9 @@ public void before(Scenario scenario) {
requestHeaders = new HashMap<>();
responseHeaders = new HashMap<>();
- request = new HttpMessage();
- response = new HttpMessage();
+ requestParams = new HashMap<>();
+ requestMessageType = Citrus.DEFAULT_MESSAGE_TYPE;
+ responseMessageType = Citrus.DEFAULT_MESSAGE_TYPE;
requestBody = null;
responseBody = null;
bodyValidationExpressions = new HashMap<>();
@@ -79,6 +80,10 @@ public void setServer(String id) {
@Then("^(?:expect|verify) HTTP request header: ([^\\s]+)(?:=| is )\"(.+)\"$")
public void addRequestHeader(String name, String value) {
+ if (name.equals(HttpHeaders.CONTENT_TYPE)) {
+ requestMessageType = getMessageType(value);
+ }
+
requestHeaders.put(name, value);
}
@@ -88,8 +93,17 @@ public void addRequestHeaders(DataTable headers) {
headerPairs.forEach(this::addRequestHeader);
}
+ @Given("^(?:expect|verify) HTTP request query parameter ([^\\s]+)(?:=| is )\"(.+)\"$")
+ public void addRequestQueryParam(String name, String value) {
+ requestParams.put(name, value);
+ }
+
@Given("^HTTP response header: ([^\\s]+)(?:=| is )\"(.+)\"$")
public void addResponseHeader(String name, String value) {
+ if (name.equals(HttpHeaders.CONTENT_TYPE)) {
+ responseMessageType = getMessageType(value);
+ }
+
responseHeaders.put(name, value);
}
@@ -147,41 +161,15 @@ public void receiveServerRequestMultilineBody(String method) {
@When("^receive (GET|HEAD|POST|PUT|PATCH|DELETE|OPTIONS|TRACE) ([^\"\\s]+)$")
public void receiveServerRequest(String method, String path) {
- request.method(HttpMethod.valueOf(method));
-
- if (StringUtils.hasText(path)) {
- request.path(path);
- request.contextPath(path);
- }
-
- if (StringUtils.hasText(requestBody)) {
- request.setPayload(requestBody);
- }
-
- for (Map.Entry headerEntry : requestHeaders.entrySet()) {
- request.setHeader(headerEntry.getKey(), headerEntry.getValue());
- }
-
- receiveServerRequest(request);
-
+ receiveServerRequest(createRequest(requestBody, requestHeaders, requestParams, method, path));
requestBody = null;
requestHeaders.clear();
+ requestParams.clear();
}
@Then("^send HTTP (\\d+)(?: [^\\s]+)?$")
public void sendServerResponse(Integer status) {
- response.status(HttpStatus.valueOf(status));
-
- if (StringUtils.hasText(responseBody)) {
- response.setPayload(responseBody);
- }
-
- for (Map.Entry headerEntry : responseHeaders.entrySet()) {
- response.setHeader(headerEntry.getKey(), headerEntry.getValue());
- }
-
- sendServerResponse(response);
-
+ sendServerResponse(createResponse(responseBody, responseHeaders, status));
responseBody = null;
responseHeaders.clear();
}
@@ -219,6 +207,8 @@ private void receiveServerRequest(HttpMessage request) {
requestBuilder.validate(headerEntry.getKey(), headerEntry.getValue());
}
bodyValidationExpressions.clear();
+
+ requestBuilder.messageType(requestMessageType);
};
runner.http(action);
@@ -231,6 +221,7 @@ private void receiveServerRequest(HttpMessage request) {
private void sendServerResponse(HttpMessage response) {
runner.http(action -> action.server(httpServer).send()
.response(response.getStatusCode())
+ .messageType(responseMessageType)
.message(response));
}
diff --git a/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpSteps.java b/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpSteps.java
new file mode 100644
index 00000000..7b34639e
--- /dev/null
+++ b/java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpSteps.java
@@ -0,0 +1,105 @@
+package dev.yaks.testing.http;
+
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import com.consol.citrus.Citrus;
+import com.consol.citrus.http.message.HttpMessage;
+import com.consol.citrus.message.MessageType;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.util.StringUtils;
+
+/**
+ * @author Christoph Deppisch
+ */
+public interface HttpSteps {
+
+ /**
+ * Maps content type value to Citrus message type used later on for selecting
+ * the right message validator implementation.
+ *
+ * @param contentType
+ * @return
+ */
+ default String getMessageType(String contentType) {
+ List binaryMediaTypes = Arrays.asList(MediaType.APPLICATION_OCTET_STREAM,
+ MediaType.APPLICATION_PDF,
+ MediaType.IMAGE_GIF,
+ MediaType.IMAGE_JPEG,
+ MediaType.IMAGE_PNG,
+ MediaType.valueOf("application/zip"));
+
+ if (contentType.equals(MediaType.APPLICATION_JSON_VALUE) ||
+ contentType.equals(MediaType.APPLICATION_JSON_UTF8_VALUE)) {
+ return MessageType.JSON.name();
+ } else if (contentType.equals(MediaType.APPLICATION_XML_VALUE)) {
+ return MessageType.XML.name();
+ } else if (contentType.equals(MediaType.APPLICATION_XHTML_XML_VALUE)) {
+ return MessageType.XHTML.name();
+ } else if (contentType.equals(MediaType.TEXT_PLAIN_VALUE) ||
+ contentType.equals(MediaType.TEXT_HTML_VALUE)) {
+ return MessageType.PLAINTEXT.name();
+ } else if (binaryMediaTypes.stream().anyMatch(mediaType -> contentType.equals(mediaType.getType()))) {
+ return MessageType.BINARY.name();
+ }
+
+ return Citrus.DEFAULT_MESSAGE_TYPE;
+ }
+
+ /**
+ * Prepare request message with given body, headers, method and path.
+ * @param body
+ * @param headers
+ * @param method
+ * @param path
+ * @return
+ */
+ default HttpMessage createRequest(String body, Map headers, Map params, String method, String path) {
+ HttpMessage request = new HttpMessage();
+ request.method(HttpMethod.valueOf(method));
+
+ if (StringUtils.hasText(path)) {
+ request.path(path);
+ request.contextPath(path);
+ }
+
+ if (StringUtils.hasText(body)) {
+ request.setPayload(body);
+ }
+
+ for (Map.Entry headerEntry : headers.entrySet()) {
+ request.setHeader(headerEntry.getKey(), headerEntry.getValue());
+ }
+
+ for (Map.Entry paramEntry : params.entrySet()) {
+ request.queryParam(paramEntry.getKey(), paramEntry.getValue());
+ }
+
+ return request;
+ }
+
+ /**
+ * Prepare response message with given body, headers and status.
+ * @param body
+ * @param headers
+ * @param status
+ * @return
+ */
+ default HttpMessage createResponse(String body, Map headers, Integer status) {
+ HttpMessage response = new HttpMessage();
+ response.status(HttpStatus.valueOf(status));
+
+ if (StringUtils.hasText(body)) {
+ response.setPayload(body);
+ }
+
+ for (Map.Entry headerEntry : headers.entrySet()) {
+ response.setHeader(headerEntry.getKey(), headerEntry.getValue());
+ }
+
+ return response;
+ }
+}
diff --git a/java/yaks-testing-swagger/pom.xml b/java/yaks-testing-swagger/pom.xml
new file mode 100644
index 00000000..7da57a94
--- /dev/null
+++ b/java/yaks-testing-swagger/pom.xml
@@ -0,0 +1,88 @@
+
+
+
+
+ dev.yaks
+ yaks-parent
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ yaks-testing-swagger
+
+
+
+
+ dev.yaks
+ yaks-testing-http
+
+
+
+ io.cucumber
+ cucumber-java
+ ${cucumber.version}
+
+
+
+ io.swagger
+ swagger-parser
+ ${swagger.parser.version}
+
+
+
+ com.consol.citrus
+ citrus-core
+ ${citrus.version}
+
+
+
+ com.consol.citrus
+ citrus-java-dsl
+ ${citrus.version}
+
+
+
+
+ junit
+ junit
+ test
+
+
+ io.cucumber
+ cucumber-junit
+ ${cucumber.version}
+ test
+
+
+ com.consol.citrus
+ citrus-cucumber
+ ${citrus.version}
+ test
+
+
+ org.apache.logging.log4j
+ log4j-slf4j-impl
+ test
+
+
+
+
+
diff --git a/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerResourceLoader.java b/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerResourceLoader.java
new file mode 100644
index 00000000..c6bbd084
--- /dev/null
+++ b/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerResourceLoader.java
@@ -0,0 +1,111 @@
+package dev.yaks.testing.swagger;
+
+import javax.net.ssl.HttpsURLConnection;
+import javax.net.ssl.SSLContext;
+import java.io.IOException;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Objects;
+
+import com.consol.citrus.util.FileUtils;
+import io.swagger.models.Swagger;
+import io.swagger.parser.SwaggerParser;
+import org.apache.http.conn.ssl.NoopHostnameVerifier;
+import org.apache.http.conn.ssl.TrustAllStrategy;
+import org.apache.http.ssl.SSLContexts;
+
+/**
+ * Loads Swagger Open API specifications from different locations like file resource or web resource.
+ * @author Christoph Deppisch
+ */
+public final class SwaggerResourceLoader {
+
+ /**
+ * Prevent instantiation of utility class.
+ */
+ private SwaggerResourceLoader() {
+ super();
+ }
+
+ /**
+ * Loads the specification from a file resource. Either classpath or file system resource path is supported.
+ * @param resource
+ * @return
+ */
+ public static Swagger fromFile(String resource) {
+ try {
+ return new SwaggerParser().parse(FileUtils.readToString(FileUtils.getFileResource(resource)));
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to parse Swagger Open API specification: " + resource, e);
+ }
+ }
+
+ /**
+ * Loads specification from given web URL location.
+ * @param url
+ * @return
+ */
+ public static Swagger fromWebResource(URL url) {
+ HttpURLConnection con = null;
+ try {
+ con = (HttpURLConnection) url.openConnection();
+ con.setRequestMethod("GET");
+
+ int status = con.getResponseCode();
+ if (status > 299) {
+ throw new IllegalStateException("Failed to retrieve Swagger Open API specification: " + url.toString(),
+ new IOException(FileUtils.readToString(con.getErrorStream())));
+ } else {
+ return new SwaggerParser().parse(FileUtils.readToString(con.getInputStream()));
+ }
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to retrieve Swagger Open API specification: " + url.toString(), e);
+ } finally {
+ if (con != null) {
+ con.disconnect();
+ }
+ }
+ }
+
+ /**
+ * Loads specification from given web URL location using secured Http connection.
+ * @param url
+ * @return
+ */
+ public static Swagger fromSecuredWebResource(URL url) {
+ Objects.requireNonNull(url);
+
+ HttpsURLConnection con = null;
+ try {
+ SSLContext sslcontext = SSLContexts
+ .custom()
+ .loadTrustMaterial(TrustAllStrategy.INSTANCE)
+ .build();
+
+ HttpsURLConnection.setDefaultSSLSocketFactory(sslcontext.getSocketFactory());
+ HttpsURLConnection.setDefaultHostnameVerifier(NoopHostnameVerifier.INSTANCE);
+
+ con = (HttpsURLConnection) url.openConnection();
+ con.setRequestMethod("GET");
+
+ int status = con.getResponseCode();
+ if (status > 299) {
+ throw new IllegalStateException("Failed to retrieve Swagger Open API specification: " + url.toString(),
+ new IOException(FileUtils.readToString(con.getErrorStream())));
+ } else {
+ return new SwaggerParser().parse(FileUtils.readToString(con.getInputStream()));
+ }
+ } catch (NoSuchAlgorithmException | KeyStoreException | KeyManagementException e) {
+ throw new IllegalStateException("Failed to create https client for ssl connection", e);
+ } catch (IOException e) {
+ throw new IllegalStateException("Failed to retrieve Swagger Open API specification: " + url.toString(), e);
+ } finally {
+ if (con != null) {
+ con.disconnect();
+ }
+ }
+ }
+}
diff --git a/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerSteps.java b/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerSteps.java
new file mode 100644
index 00000000..cde17523
--- /dev/null
+++ b/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerSteps.java
@@ -0,0 +1,258 @@
+package dev.yaks.testing.swagger;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.regex.Pattern;
+import java.util.stream.Collectors;
+
+import com.consol.citrus.Citrus;
+import com.consol.citrus.annotations.CitrusAnnotations;
+import com.consol.citrus.annotations.CitrusFramework;
+import com.consol.citrus.annotations.CitrusResource;
+import com.consol.citrus.dsl.annotations.CitrusDslAnnotations;
+import com.consol.citrus.dsl.runner.TestRunner;
+import com.consol.citrus.variable.dictionary.json.JsonPathMappingDataDictionary;
+import cucumber.api.Scenario;
+import cucumber.api.java.Before;
+import cucumber.api.java.en.And;
+import cucumber.api.java.en.Given;
+import cucumber.api.java.en.Then;
+import cucumber.api.java.en.When;
+import dev.yaks.testing.http.HttpClientSteps;
+import io.cucumber.datatable.DataTable;
+import io.swagger.models.ArrayModel;
+import io.swagger.models.HttpMethod;
+import io.swagger.models.Operation;
+import io.swagger.models.Path;
+import io.swagger.models.RefModel;
+import io.swagger.models.Response;
+import io.swagger.models.Scheme;
+import io.swagger.models.Swagger;
+import io.swagger.models.parameters.BodyParameter;
+import io.swagger.models.parameters.HeaderParameter;
+import io.swagger.models.parameters.Parameter;
+import io.swagger.models.parameters.PathParameter;
+import io.swagger.models.parameters.QueryParameter;
+import io.swagger.models.properties.ArrayProperty;
+import io.swagger.models.properties.Property;
+import io.swagger.models.properties.RefProperty;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+
+public class SwaggerSteps {
+
+ @CitrusResource
+ private TestRunner runner;
+
+ @CitrusFramework
+ private Citrus citrus;
+
+ private HttpClientSteps clientSteps;
+
+ private Swagger swagger;
+ private Operation operation;
+
+ private JsonPathMappingDataDictionary outboundDictionary;
+ private JsonPathMappingDataDictionary inboundDictionary;
+
+ @Before
+ public void before(Scenario scenario) {
+ clientSteps = new HttpClientSteps();
+ CitrusAnnotations.injectAll(clientSteps, citrus);
+ CitrusDslAnnotations.injectTestRunner(clientSteps, runner);
+ clientSteps.before(scenario);
+
+ operation = null;
+
+ outboundDictionary = new JsonPathMappingDataDictionary();
+ inboundDictionary = new JsonPathMappingDataDictionary();
+ }
+
+ @Given("^OpenAPI specification: ([^\\s]+)$")
+ public void loadOpenAPISpec(String resource) {
+ loadSwaggerResource(resource);
+ }
+
+ @Given("^Swagger resource: ([^\\s]+)$")
+ public void loadSwaggerResource(String resource) {
+ if (resource.startsWith("http")) {
+ try {
+ URL url = new URL(resource);
+ if (resource.startsWith("https")) {
+ swagger = SwaggerResourceLoader.fromSecuredWebResource(url);
+ } else {
+ swagger = SwaggerResourceLoader.fromWebResource(url);
+ }
+ clientSteps.setUrl(String.format("%s://%s%s/%s", url.getProtocol(), url.getHost(), url.getPort() > 0 ? ":" + url.getPort() : "", getBasePath()));
+ } catch (MalformedURLException e) {
+ throw new IllegalStateException("Failed to retrieve Swagger Open API specification as web resource: " + resource, e);
+ }
+ } else {
+ SwaggerResourceLoader.fromFile(resource);
+
+ Scheme scheme = Optional.ofNullable(swagger.getSchemes())
+ .orElse(Collections.singletonList(Scheme.HTTP))
+ .stream()
+ .filter(s -> s == Scheme.HTTP || s == Scheme.HTTPS)
+ .findFirst()
+ .orElse(Scheme.HTTP);
+
+ clientSteps.setUrl(String.format("%s://%s/%s", scheme.toValue(), swagger.getHost(), getBasePath()));
+ }
+ }
+
+ @Given("^outbound dictionary$")
+ public void createOutboundDictionary(DataTable dataTable) {
+ Map mappings = dataTable.asMap(String.class, String.class);
+ for (Map.Entry mapping : mappings.entrySet()) {
+ outboundDictionary.getMappings().put(mapping.getKey(), mapping.getValue());
+ }
+ }
+
+ @Given("^inbound dictionary$")
+ public void createInboundDictionary(DataTable dataTable) {
+ Map mappings = dataTable.asMap(String.class, String.class);
+ for (Map.Entry mapping : mappings.entrySet()) {
+ inboundDictionary.getMappings().put(mapping.getKey(), mapping.getValue());
+ }
+ }
+
+ @When("^(?:I|i)nvoke operation: (.+)$")
+ public void invokeOperation(String operationId) {
+ for (Map.Entry path : swagger.getPaths().entrySet()) {
+ Optional> operationEntry = path.getValue().getOperationMap().entrySet().stream()
+ .filter(op -> operationId.equals(op.getValue().getOperationId()))
+ .findFirst();
+
+ if (operationEntry.isPresent()) {
+ operation = operationEntry.get().getValue();
+ sendRequest(path.getKey(), operationEntry.get().getKey(), operationEntry.get().getValue());
+ break;
+ }
+ }
+ }
+
+ @Then("^(?:V|v)erify operation result: (\\d+)(?: [^\\s]+)?$")
+ public void verifyResponseByStatus(int response) {
+ receiveResponse(operation, String.valueOf(response));
+ }
+
+ @And("^(?:V|v)erify operation response: (.+)$")
+ public void verifyResponseByName(String response) {
+ receiveResponse(operation, response);
+ }
+
+ /**
+ * Invoke request for given API operation. The request parameters, headers and payload are generated via specification
+ * details in that operation.
+ * @param path
+ * @param method
+ * @param operation
+ */
+ private void sendRequest(String path, HttpMethod method, Operation operation) {
+ if (operation.getParameters() != null) {
+ operation.getParameters().stream()
+ .filter(p -> p instanceof HeaderParameter)
+ .filter(Parameter::getRequired)
+ .forEach(p -> clientSteps.addRequestHeader(p.getName(), SwaggerTestDataGenerator.createRandomValueExpression(((HeaderParameter) p).getItems(), swagger.getDefinitions(), false)));
+
+ operation.getParameters().stream()
+ .filter(param -> param instanceof QueryParameter)
+ .filter(Parameter::getRequired)
+ .forEach(param -> clientSteps.addRequestQueryParam(param.getName(), SwaggerTestDataGenerator.createRandomValueExpression((QueryParameter) param)));
+
+ operation.getParameters().stream()
+ .filter(p -> p instanceof BodyParameter)
+ .filter(Parameter::getRequired)
+ .findFirst()
+ .ifPresent(p -> {
+ BodyParameter body = (BodyParameter) p;
+ if (body.getSchema() != null) {
+ clientSteps.setRequestBody(SwaggerTestDataGenerator.createOutboundPayload(body.getSchema(), swagger.getDefinitions()));
+
+ if ((body.getSchema() instanceof RefModel || body.getSchema() instanceof ArrayModel)) {
+ clientSteps.setOutboundDictionary(outboundDictionary);
+ }
+ }
+ });
+ }
+
+ String randomizedPath = path;
+ if (operation.getParameters() != null) {
+ List pathParams = operation.getParameters().stream()
+ .filter(p -> p instanceof PathParameter)
+ .map(PathParameter.class::cast)
+ .collect(Collectors.toList());
+
+ for (PathParameter parameter : pathParams) {
+ String parameterValue;
+ if (runner.getTestCase().getVariableDefinitions().containsKey(parameter.getName())) {
+ parameterValue = "\\" + Citrus.VARIABLE_PREFIX + parameter.getName() + Citrus.VARIABLE_SUFFIX;
+ } else {
+ parameterValue = SwaggerTestDataGenerator.createRandomValueExpression(parameter);
+ }
+ randomizedPath = Pattern.compile("\\{" + parameter.getName() + "}")
+ .matcher(randomizedPath)
+ .replaceAll(parameterValue);
+ }
+ }
+
+ if (operation.getConsumes() != null) {
+ clientSteps.addRequestHeader(HttpHeaders.CONTENT_TYPE, operation.getConsumes().get(0));
+ }
+
+ clientSteps.sendClientRequest(method.name().toUpperCase(), randomizedPath);
+ }
+
+ /**
+ * Verify operation response where expected parameters, headers and payload are generated using the operation specification details.
+ * @param operation
+ * @param status
+ */
+ private void receiveResponse(Operation operation, String status) {
+ if (operation.getResponses() != null) {
+ Response response = Optional.ofNullable(operation.getResponses().get(status))
+ .orElse(operation.getResponses().get("default"));
+
+ if (response != null) {
+ if (response.getHeaders() != null) {
+ for (Map.Entry header : response.getHeaders().entrySet()) {
+ clientSteps.addResponseHeader(header.getKey(), SwaggerTestDataGenerator.createValidationExpression(header.getValue(), swagger.getDefinitions(), false));
+ }
+ }
+
+ if (response.getSchema() != null) {
+ clientSteps.setResponseBody(SwaggerTestDataGenerator.createInboundPayload(response.getSchema(), swagger.getDefinitions()));
+
+ if ((response.getSchema() instanceof RefProperty || response.getSchema() instanceof ArrayProperty)) {
+ clientSteps.setInboundDictionary(inboundDictionary);
+ }
+ }
+ }
+ }
+
+ if (operation.getProduces() != null) {
+ clientSteps.addResponseHeader(HttpHeaders.CONTENT_TYPE, operation.getProduces().get(0));
+ }
+
+ if (Pattern.compile("[0-9]+").matcher(status).matches()) {
+ clientSteps.receiveClientResponse(Integer.parseInt(status));
+ } else {
+ clientSteps.receiveClientResponse(HttpStatus.OK.value());
+ }
+ }
+
+ /**
+ * Gets the normalized base path of the service.
+ * @return
+ */
+ private String getBasePath() {
+ return Optional.ofNullable(swagger.getBasePath())
+ .map(basePath -> basePath.startsWith("/") ? basePath.substring(1) : basePath).orElse("");
+ }
+
+}
diff --git a/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerTestDataGenerator.java b/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerTestDataGenerator.java
new file mode 100644
index 00000000..934ed863
--- /dev/null
+++ b/java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerTestDataGenerator.java
@@ -0,0 +1,398 @@
+package dev.yaks.testing.swagger;
+
+import java.util.Map;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import io.swagger.models.ArrayModel;
+import io.swagger.models.Model;
+import io.swagger.models.RefModel;
+import io.swagger.models.parameters.AbstractSerializableParameter;
+import io.swagger.models.properties.ArrayProperty;
+import io.swagger.models.properties.BooleanProperty;
+import io.swagger.models.properties.DateProperty;
+import io.swagger.models.properties.DateTimeProperty;
+import io.swagger.models.properties.DoubleProperty;
+import io.swagger.models.properties.FloatProperty;
+import io.swagger.models.properties.IntegerProperty;
+import io.swagger.models.properties.LongProperty;
+import io.swagger.models.properties.Property;
+import io.swagger.models.properties.RefProperty;
+import io.swagger.models.properties.StringProperty;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+/**
+ * Generates proper payloads and validation expressions based on Swagger Open API specification rules. Creates outbound payloads
+ * with generated random test data according to specification and creates inbound payloads with proper validation expressions to
+ * enforce the specification rules.
+ *
+ * @author Christoph Deppisch
+ */
+public class SwaggerTestDataGenerator {
+
+ /**
+ * Creates payload from schema for outbound message.
+ * @param model
+ * @param definitions
+ * @return
+ */
+ public static String createOutboundPayload(Model model, Map definitions) {
+ StringBuilder payload = new StringBuilder();
+
+ if (model instanceof RefModel) {
+ model = definitions.get(((RefModel) model).getSimpleRef());
+ }
+
+ if (model instanceof ArrayModel) {
+ payload.append(createOutboundPayload(((ArrayModel) model).getItems(), definitions));
+ } else {
+ payload.append("{");
+
+ if (model.getProperties() != null) {
+ for (Map.Entry entry : model.getProperties().entrySet()) {
+ payload.append("\"").append(entry.getKey()).append("\": ").append(createOutboundPayload(entry.getValue(), definitions)).append(",");
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Creates payload from property for outbound message.
+ * @param property
+ * @param definitions
+ * @return
+ */
+ public static String createOutboundPayload(Property property, Map definitions) {
+ StringBuilder payload = new StringBuilder();
+
+ if (property instanceof RefProperty) {
+ Model model = definitions.get(((RefProperty) property).getSimpleRef());
+ payload.append("{");
+
+ if (model.getProperties() != null) {
+ for (Map.Entry entry : model.getProperties().entrySet()) {
+ payload.append("\"")
+ .append(entry.getKey())
+ .append("\": ")
+ .append(createRandomValueExpression(entry.getValue(), definitions, true))
+ .append(",");
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ } else if (property instanceof ArrayProperty) {
+ payload.append("[");
+ payload.append(createRandomValueExpression(((ArrayProperty) property).getItems(), definitions, true));
+ payload.append("]");
+ } else {
+ payload.append(createRandomValueExpression(property, definitions, true));
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Create payload from schema with random values.
+ * @param property
+ * @param definitions
+ * @param quotes
+ * @return
+ */
+ public static String createRandomValueExpression(Property property, Map definitions, boolean quotes) {
+ StringBuilder payload = new StringBuilder();
+
+ if (property instanceof RefProperty) {
+ payload.append(createOutboundPayload(property, definitions));
+ } else if (property instanceof ArrayProperty) {
+ payload.append(createOutboundPayload(property, definitions));
+ } else if (property instanceof StringProperty || property instanceof DateProperty || property instanceof DateTimeProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ if (property instanceof DateProperty) {
+ payload.append("citrus:currentDate()");
+ } else if (property instanceof DateTimeProperty) {
+ payload.append("citrus:currentDate('yyyy-MM-dd'T'hh:mm:ss')");
+ } else if (!CollectionUtils.isEmpty(((StringProperty) property).getEnum())) {
+ payload.append("citrus:randomEnumValue(").append(((StringProperty) property).getEnum().stream().map(value -> "'" + value + "'").collect(Collectors.joining(","))).append(")");
+ } else if (Optional.ofNullable(property.getFormat()).orElse("").equalsIgnoreCase("uuid")) {
+ payload.append("citrus:randomUUID()");
+ } else {
+ payload.append("citrus:randomString(").append(((StringProperty) property).getMaxLength() != null && ((StringProperty) property).getMaxLength() > 0 ? ((StringProperty) property).getMaxLength() : (((StringProperty) property).getMinLength() != null && ((StringProperty) property).getMinLength() > 0 ? ((StringProperty) property).getMinLength() : 10)).append(")");
+ }
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof IntegerProperty || property instanceof LongProperty) {
+ payload.append("citrus:randomNumber(10)");
+ } else if (property instanceof FloatProperty || property instanceof DoubleProperty) {
+ payload.append("citrus:randomNumber(10)");
+ } else if (property instanceof BooleanProperty) {
+ payload.append("citrus:randomEnumValue('true', 'false')");
+ } else if (quotes) {
+ payload.append("\"\"");
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Creates control payload from property for validation.
+ * @param property
+ * @param definitions
+ * @return
+ */
+ public static String createInboundPayload(Property property, Map definitions) {
+ StringBuilder payload = new StringBuilder();
+
+ if (property instanceof RefProperty) {
+ Model model = definitions.get(((RefProperty) property).getSimpleRef());
+ payload.append("{");
+
+ if (model.getProperties() != null) {
+ for (Map.Entry entry : model.getProperties().entrySet()) {
+ payload.append("\"").append(entry.getKey()).append("\": ").append(createValidationExpression(entry.getValue(), definitions, true)).append(",");
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ } else if (property instanceof ArrayProperty) {
+ payload.append("[");
+ payload.append(createValidationExpression(((ArrayProperty) property).getItems(), definitions, true));
+ payload.append("]");
+ } else {
+ payload.append(createValidationExpression(property, definitions, false));
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Creates control payload from schema for validation.
+ * @param model
+ * @param definitions
+ * @return
+ */
+ public static String createInboundPayload(Model model, Map definitions) {
+ StringBuilder payload = new StringBuilder();
+
+ if (model instanceof RefModel) {
+ model = definitions.get(((RefModel) model).getSimpleRef());
+ }
+
+ if (model instanceof ArrayModel) {
+ payload.append("[");
+ payload.append(createValidationExpression(((ArrayModel) model).getItems(), definitions, true));
+ payload.append("]");
+ } else {
+ payload.append("{");
+
+ if (model.getProperties() != null) {
+ for (Map.Entry entry : model.getProperties().entrySet()) {
+ payload.append("\"").append(entry.getKey()).append("\": ").append(createValidationExpression(entry.getValue(), definitions, true)).append(",");
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Create validation expression using functions according to parameter type and format.
+ * @param property
+ * @param definitions
+ * @param quotes
+ * @return
+ */
+ public static String createValidationExpression(Property property, Map definitions, boolean quotes) {
+ StringBuilder payload = new StringBuilder();
+ if (property instanceof RefProperty) {
+ Model model = definitions.get(((RefProperty) property).getSimpleRef());
+ payload.append("{");
+
+ if (model.getProperties() != null) {
+ for (Map.Entry entry : model.getProperties().entrySet()) {
+ payload.append("\"").append(entry.getKey()).append("\": ").append(createValidationExpression(entry.getValue(), definitions, quotes)).append(",");
+ }
+ }
+
+ if (payload.toString().endsWith(",")) {
+ payload.replace(payload.length() - 1, payload.length(), "");
+ }
+
+ payload.append("}");
+ } else if (property instanceof ArrayProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@ignore@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof StringProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ if (StringUtils.hasText(((StringProperty) property).getPattern())) {
+ payload.append("@matches(").append(((StringProperty) property).getPattern()).append(")@");
+ } else if (!CollectionUtils.isEmpty(((StringProperty) property).getEnum())) {
+ payload.append("@matches(").append(((StringProperty) property).getEnum().stream().collect(Collectors.joining("|"))).append(")@");
+ } else {
+ payload.append("@notEmpty()@");
+ }
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof DateProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@matchesDatePattern('yyyy-MM-dd')@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof DateTimeProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ss')@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof IntegerProperty || property instanceof LongProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@isNumber()@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof FloatProperty || property instanceof DoubleProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@isNumber()@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else if (property instanceof BooleanProperty) {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@matches(true|false)@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ } else {
+ if (quotes) {
+ payload.append("\"");
+ }
+
+ payload.append("@ignore@");
+
+ if (quotes) {
+ payload.append("\"");
+ }
+ }
+
+ return payload.toString();
+ }
+
+ /**
+ * Create validation expression using functions according to parameter type and format.
+ * @param parameter
+ * @return
+ */
+ public static String createValidationExpression(AbstractSerializableParameter parameter) {
+ switch (parameter.getType()) {
+ case "integer":
+ return "@isNumber()@";
+ case "string":
+ if (parameter.getFormat() != null && parameter.getFormat().equals("date")) {
+ return "\"@matchesDatePattern('yyyy-MM-dd')@\"";
+ } else if (parameter.getFormat() != null && parameter.getFormat().equals("date-time")) {
+ return "\"@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ss')@\"";
+ } else if (StringUtils.hasText(parameter.getPattern())) {
+ return "\"@matches(" + parameter.getPattern() + ")@\"";
+ } else if (!CollectionUtils.isEmpty(parameter.getEnum())) {
+ return "\"@matches(" + (parameter.getEnum().stream().collect(Collectors.joining("|"))) + ")@\"";
+ } else {
+ return "@notEmpty()@";
+ }
+ case "boolean":
+ return "@matches(true|false)@";
+ default:
+ return "@ignore@";
+ }
+ }
+
+ /**
+ * Create random value expression using functions according to parameter type and format.
+ * @param parameter
+ * @return
+ */
+ public static String createRandomValueExpression(AbstractSerializableParameter parameter) {
+ switch (parameter.getType()) {
+ case "integer":
+ return "citrus:randomNumber(10)";
+ case "string":
+ if (parameter.getFormat() != null && parameter.getFormat().equals("date")) {
+ return "\"citrus:currentDate('yyyy-MM-dd')\"";
+ } else if (parameter.getFormat() != null && parameter.getFormat().equals("date-time")) {
+ return "\"citrus:currentDate('yyyy-MM-dd'T'hh:mm:ss')\"";
+ } else if (StringUtils.hasText(parameter.getPattern())) {
+ return "\"citrus:randomValue(" + parameter.getPattern() + ")\"";
+ } else if (!CollectionUtils.isEmpty(parameter.getEnum())) {
+ return "\"citrus:randomEnumValue(" + (parameter.getEnum().stream().collect(Collectors.joining(","))) + ")\"";
+ } else if (Optional.ofNullable(parameter.getFormat()).orElse("").equalsIgnoreCase("uuid")){
+ return "citrus:randomUUID()";
+ } else {
+ return "citrus:randomString(10)";
+ }
+ case "boolean":
+ return "true";
+ default:
+ return "";
+ }
+ }
+
+}
diff --git a/java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/PetstoreConfiguration.java b/java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/PetstoreConfiguration.java
new file mode 100644
index 00000000..1359c53d
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/PetstoreConfiguration.java
@@ -0,0 +1,140 @@
+package dev.yaks.testing.swagger;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Optional;
+
+import com.consol.citrus.context.TestContext;
+import com.consol.citrus.dsl.endpoint.CitrusEndpoints;
+import com.consol.citrus.endpoint.EndpointAdapter;
+import com.consol.citrus.endpoint.adapter.RequestDispatchingEndpointAdapter;
+import com.consol.citrus.endpoint.adapter.StaticEndpointAdapter;
+import com.consol.citrus.endpoint.adapter.StaticResponseEndpointAdapter;
+import com.consol.citrus.endpoint.adapter.mapping.HeaderMappingKeyExtractor;
+import com.consol.citrus.endpoint.adapter.mapping.SimpleMappingStrategy;
+import com.consol.citrus.http.message.HttpMessage;
+import com.consol.citrus.http.message.HttpMessageHeaders;
+import com.consol.citrus.http.server.HttpServer;
+import com.consol.citrus.message.Message;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+
+/**
+ * @author Christoph Deppisch
+ */
+@Configuration
+public class PetstoreConfiguration {
+
+ private static final int HTTP_PORT = 8080;
+
+ @Bean
+ public HttpServer petstoreServer() {
+ return CitrusEndpoints.http()
+ .server()
+ .port(HTTP_PORT)
+ .autoStart(true)
+ .endpointAdapter(staticResponseAdapter())
+ .build();
+ }
+
+ @Bean
+ public EndpointAdapter staticResponseAdapter() {
+ RequestDispatchingEndpointAdapter dispatchingEndpointAdapter = new RequestDispatchingEndpointAdapter();
+
+ Map mappings = new HashMap<>();
+
+ mappings.put(HttpMethod.GET.name(), handleGetRequestAdapter());
+ mappings.put(HttpMethod.POST.name(), handlePostRequestAdapter());
+ mappings.put(HttpMethod.PUT.name(), handlePutRequestAdapter());
+ mappings.put(HttpMethod.DELETE.name(), handleDeleteRequestAdapter());
+
+ SimpleMappingStrategy mappingStrategy = new SimpleMappingStrategy();
+ mappingStrategy.setAdapterMappings(mappings);
+ dispatchingEndpointAdapter.setMappingStrategy(mappingStrategy);
+
+ dispatchingEndpointAdapter.setMappingKeyExtractor(new HeaderMappingKeyExtractor(HttpMessageHeaders.HTTP_REQUEST_METHOD));
+
+ return dispatchingEndpointAdapter;
+ }
+
+ @Bean
+ public EndpointAdapter handlePostRequestAdapter() {
+ return new StaticEndpointAdapter() {
+ @Override
+ protected Message handleMessageInternal(Message message) {
+ return new HttpMessage()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .status(HttpStatus.CREATED);
+ }
+ };
+ }
+
+ @Bean
+ public EndpointAdapter handlePutRequestAdapter() {
+ return new StaticEndpointAdapter() {
+ @Override
+ protected Message handleMessageInternal(Message request) {
+ return new HttpMessage()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .status(HttpStatus.OK);
+ }
+ };
+ }
+
+ @Bean
+ public EndpointAdapter handleGetRequestAdapter() {
+ return new StaticResponseEndpointAdapter() {
+ private TestContext context;
+
+ @Override
+ public Message handleMessageInternal(Message request) {
+ context = super.getTestContext();
+ getMessageHeader().clear();
+ setMessagePayload("");
+
+ String requestUri = Optional.ofNullable(request.getHeader(HttpMessageHeaders.HTTP_REQUEST_URI))
+ .map(Object::toString)
+ .orElse("/openapi.json");
+
+ if (requestUri.endsWith("openapi.json")) {
+ setMessagePayload("citrus:readFile('classpath:dev/yaks/testing/swagger/petstore-api.json')");
+ } else {
+ int petId = Integer.parseInt(requestUri.substring(requestUri.lastIndexOf("/") + 1));
+ getMessageHeader().put(HttpMessageHeaders.HTTP_CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
+
+ if (petId > 0) {
+ getTestContext().setVariable("petId", petId);
+ setMessagePayload("citrus:readFile('classpath:dev/yaks/testing/swagger/pet.json')");
+ } else {
+ getMessageHeader().put(HttpMessageHeaders.HTTP_STATUS_CODE, HttpStatus.NOT_FOUND);
+ }
+ }
+
+ return super.handleMessageInternal(request);
+ }
+
+ @Override
+ protected TestContext getTestContext() {
+ if (context == null) {
+ context = super.getTestContext();
+ }
+ return context;
+ }
+ };
+ }
+
+ @Bean
+ public EndpointAdapter handleDeleteRequestAdapter() {
+ return new StaticEndpointAdapter() {
+ @Override
+ protected Message handleMessageInternal(Message message) {
+ return new HttpMessage()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .status(HttpStatus.NO_CONTENT);
+ }
+ };
+ }
+}
diff --git a/java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/SwaggerFeatureTest.java b/java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/SwaggerFeatureTest.java
new file mode 100644
index 00000000..83219287
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/SwaggerFeatureTest.java
@@ -0,0 +1,18 @@
+package dev.yaks.testing.swagger;
+
+import cucumber.api.CucumberOptions;
+import cucumber.api.junit.Cucumber;
+import org.junit.runner.RunWith;
+
+/**
+ * @author Christoph Deppisch
+ */
+@RunWith(Cucumber.class)
+@CucumberOptions(
+ strict = true,
+ glue = { "com.consol.citrus.cucumber.step.runner.core",
+ "dev.yaks.testing.swagger",
+ "dev.yaks.testing.http" },
+ plugin = { "com.consol.citrus.cucumber.CitrusReporter" } )
+public class SwaggerFeatureTest {
+}
diff --git a/java/yaks-testing-swagger/src/test/resources/citrus-application.properties b/java/yaks-testing-swagger/src/test/resources/citrus-application.properties
new file mode 100644
index 00000000..c62e8d19
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/citrus-application.properties
@@ -0,0 +1 @@
+citrus.spring.java.config=dev.yaks.testing.swagger.PetstoreConfiguration
diff --git a/java/yaks-testing-swagger/src/test/resources/cucumber.properties b/java/yaks-testing-swagger/src/test/resources/cucumber.properties
new file mode 100644
index 00000000..3ee1337b
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/cucumber.properties
@@ -0,0 +1 @@
+cucumber.api.java.ObjectFactory=cucumber.runtime.java.CitrusObjectFactory
diff --git a/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/pet.json b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/pet.json
new file mode 100644
index 00000000..0d4e504a
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/pet.json
@@ -0,0 +1,16 @@
+{
+ "id": ${petId},
+ "name": "citrus:randomEnumValue('hasso','cutie','fluffy')",
+ "category": {
+ "id": ${petId},
+ "name": "citrus:randomEnumValue('dog', 'cat', 'fish')"
+ },
+ "photoUrls": [ "http://localhost:8080/photos/${petId}" ],
+ "tags": [
+ {
+ "id": ${petId},
+ "name": "generated"
+ }
+ ],
+ "status": "citrus:randomEnumValue('available', 'pending', 'sold')"
+}
diff --git a/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-api.json b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-api.json
new file mode 100644
index 00000000..f3e5829c
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-api.json
@@ -0,0 +1,279 @@
+{
+ "swagger": "2.0",
+ "info": {
+ "description": "This is a sample server Petstore server.",
+ "version": "1.0.1",
+ "title": "Swagger Petstore",
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ }
+ },
+ "host": "localhost",
+ "basePath": "/petstore/v2",
+ "tags": [
+ {
+ "name": "pet",
+ "description": "Everything about your Pets"
+ }
+ ],
+ "schemes": [
+ "https"
+ ],
+ "paths": {
+ "/pet": {
+ "post": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Add a new pet to the store",
+ "description": "",
+ "operationId": "addPet",
+ "consumes": [
+ "application/json",
+ "application/xml"
+ ],
+ "produces": [
+ "application/json",
+ "application/xml"
+ ],
+ "parameters": [
+ {
+ "in": "body",
+ "name": "body",
+ "description": "Pet object that needs to be added to the store",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/Pet"
+ }
+ }
+ ],
+ "responses": {
+ "201": {
+ "description": "Created"
+ },
+ "405": {
+ "description": "Invalid input"
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Update an existing pet",
+ "description": "",
+ "operationId": "updatePet",
+ "consumes": [
+ "application/json",
+ "application/xml"
+ ],
+ "produces": [
+ "application/json",
+ "application/xml"
+ ],
+ "parameters": [
+ {
+ "in": "body",
+ "name": "body",
+ "description": "Pet object that needs to be added to the store",
+ "required": true,
+ "schema": {
+ "$ref": "#/definitions/Pet"
+ }
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content"
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ },
+ "405": {
+ "description": "Validation exception"
+ }
+ }
+ }
+ },
+ "/pet/{petId}": {
+ "get": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Find pet by ID",
+ "description": "Returns a single pet",
+ "operationId": "getPetById",
+ "produces": [
+ "application/json",
+ "application/xml"
+ ],
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "ID of pet to return",
+ "required": true,
+ "type": "integer",
+ "format": "int64"
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Pet"
+ }
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Deletes a pet",
+ "description": "",
+ "operationId": "deletePet",
+ "produces": [
+ "application/json",
+ "application/xml"
+ ],
+ "parameters": [
+ {
+ "name": "api_key",
+ "in": "header",
+ "required": false,
+ "type": "string"
+ },
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "Pet id to delete",
+ "required": true,
+ "type": "integer",
+ "format": "int64"
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content"
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ }
+ }
+ }
+ }
+ },
+ "definitions": {
+ "Category": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "Category"
+ }
+ },
+ "Tag": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "Tag"
+ }
+ },
+ "Pet": {
+ "type": "object",
+ "required": [
+ "name",
+ "photoUrls"
+ ],
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "category": {
+ "$ref": "#/definitions/Category"
+ },
+ "name": {
+ "type": "string",
+ "example": "doggie"
+ },
+ "photoUrls": {
+ "type": "array",
+ "xml": {
+ "name": "photoUrl",
+ "wrapped": true
+ },
+ "items": {
+ "type": "string"
+ }
+ },
+ "tags": {
+ "type": "array",
+ "xml": {
+ "name": "tag",
+ "wrapped": true
+ },
+ "items": {
+ "$ref": "#/definitions/Tag"
+ }
+ },
+ "status": {
+ "type": "string",
+ "description": "pet status in the store",
+ "enum": [
+ "available",
+ "pending",
+ "sold"
+ ]
+ }
+ },
+ "xml": {
+ "name": "Pet"
+ }
+ },
+ "ApiResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "type": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+}
diff --git a/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-open-api.feature b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-open-api.feature
new file mode 100644
index 00000000..290f16cc
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-open-api.feature
@@ -0,0 +1,32 @@
+Feature: Petstore API
+
+ Background:
+ Given OpenAPI specification: http://localhost:8080/petstore/v2/openapi.json
+ Given variable petId is "citrus:randomNumber(5)"
+ Given inbound dictionary
+ | $.name | @assertThat(anyOf(is(hasso),is(cutie),is(fluffy)))@ |
+ | $.category.name | @assertThat(anyOf(is(dog),is(cat),is(fish)))@ |
+ Given outbound dictionary
+ | $.name | citrus:randomEnumValue('hasso','cutie','fluffy') |
+ | $.category.name | citrus:randomEnumValue('dog', 'cat', 'fish') |
+
+ Scenario: getPet
+ When invoke operation: getPetById
+ Then verify operation result: 200 OK
+
+ Scenario: petNotFound
+ Given variable petId is "0"
+ When invoke operation: getPetById
+ Then verify operation result: 404 NOT_FOUND
+
+ Scenario: addPet
+ When invoke operation: addPet
+ Then verify operation result: 201 CREATED
+
+ Scenario: updatePet
+ When invoke operation: updatePet
+ Then verify operation result: 200 OK
+
+ Scenario: deletePet
+ When invoke operation: deletePet
+ Then verify operation result: 204 NO_CONTENT
diff --git a/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-rest-api.feature b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-rest-api.feature
new file mode 100644
index 00000000..2470ad88
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-rest-api.feature
@@ -0,0 +1,51 @@
+Feature: REST API
+
+ Background:
+ Given URL: http://localhost:8080/petstore/v2
+
+ Scenario: getPet
+ Given variable petId is "1000"
+ When send GET /pet/${petId}
+ Then verify HTTP response body
+ """
+ {
+ "id": ${petId},
+ "name": "@matches(cutie|fluffy|hasso)@",
+ "category": {
+ "id": "@ignore@",
+ "name":"@matches(dog|cat|fish)@"
+ },
+ "status": "@matches(available|pending|sold)@",
+ "photoUrls":[
+ "http://localhost:8080/photos/${petId}"
+ ],
+ "tags":[
+ {
+ "id": "@ignore@",
+ "name":"generated"
+ }
+ ]
+ }
+ """
+ And verify HTTP response header Content-Type="application/json"
+ And receive HTTP 200 OK
+
+ Scenario: petNotFound
+ Given variable petId is "0"
+ When send GET /pet/${petId}
+ And receive HTTP 404 NOT_FOUND
+
+ Scenario: addPet
+ Given HTTP request body
+ """
+ {
+ "name": "hasso",
+ "category":{
+ "id": 1,
+ "name":"dog"
+ },
+ "status": "available"
+ }
+ """
+ When send POST /pet
+ Then receive HTTP 201 CREATED
diff --git a/java/yaks-testing-swagger/src/test/resources/log4j2-test.xml b/java/yaks-testing-swagger/src/test/resources/log4j2-test.xml
new file mode 100644
index 00000000..d55096c2
--- /dev/null
+++ b/java/yaks-testing-swagger/src/test/resources/log4j2-test.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/java/yaks-testing/pom.xml b/java/yaks-testing/pom.xml
index 8fcd0c85..c80beae2 100644
--- a/java/yaks-testing/pom.xml
+++ b/java/yaks-testing/pom.xml
@@ -56,6 +56,11 @@
yaks-testing-jdbc
+
+ dev.yaks
+ yaks-testing-swagger
+
+
dev.yaks
yaks-testing-standard
diff --git a/java/yaks-testing/src/main/java/dev/yaks/testing/TestRunner.java b/java/yaks-testing/src/main/java/dev/yaks/testing/TestRunner.java
index c7d75c33..7ed54978 100644
--- a/java/yaks-testing/src/main/java/dev/yaks/testing/TestRunner.java
+++ b/java/yaks-testing/src/main/java/dev/yaks/testing/TestRunner.java
@@ -36,6 +36,9 @@ public static void main(String[] args) throws IOException {
params.add("--glue");
params.add("dev.yaks.testing.http");
+ params.add("--glue");
+ params.add("dev.yaks.testing.swagger");
+
params.add("--glue");
params.add("dev.yaks.testing.camel.k");