From c4a15e8c79b68d4cf2e1adc035b8ff65a97ba21a Mon Sep 17 00:00:00 2001 From: Christoph Deppisch Date: Tue, 17 Sep 2019 16:33:43 +0200 Subject: [PATCH] feat: #32 Add Swagger steps Adds Open API support on generating test data for Http scenarios. Also reduces code duplications on HttpSteps. Fixes #32 --- java/pom.xml | 7 + .../yaks/testing/http/HttpClientSteps.java | 97 +++-- .../yaks/testing/http/HttpServerSteps.java | 65 ++- .../java/dev/yaks/testing/http/HttpSteps.java | 105 +++++ java/yaks-testing-swagger/pom.xml | 88 ++++ .../swagger/SwaggerResourceLoader.java | 111 +++++ .../yaks/testing/swagger/SwaggerSteps.java | 258 ++++++++++++ .../swagger/SwaggerTestDataGenerator.java | 398 ++++++++++++++++++ .../swagger/PetstoreConfiguration.java | 140 ++++++ .../testing/swagger/SwaggerFeatureTest.java | 18 + .../resources/citrus-application.properties | 1 + .../src/test/resources/cucumber.properties | 1 + .../dev/yaks/testing/swagger/pet.json | 16 + .../yaks/testing/swagger/petstore-api.json | 279 ++++++++++++ .../testing/swagger/petstore-open-api.feature | 32 ++ .../testing/swagger/petstore-rest-api.feature | 51 +++ .../src/test/resources/log4j2-test.xml | 34 ++ java/yaks-testing/pom.xml | 5 + .../java/dev/yaks/testing/TestRunner.java | 3 + 19 files changed, 1636 insertions(+), 73 deletions(-) create mode 100644 java/yaks-testing-http/src/main/java/dev/yaks/testing/http/HttpSteps.java create mode 100644 java/yaks-testing-swagger/pom.xml create mode 100644 java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerResourceLoader.java create mode 100644 java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerSteps.java create mode 100644 java/yaks-testing-swagger/src/main/java/dev/yaks/testing/swagger/SwaggerTestDataGenerator.java create mode 100644 java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/PetstoreConfiguration.java create mode 100644 java/yaks-testing-swagger/src/test/java/dev/yaks/testing/swagger/SwaggerFeatureTest.java create mode 100644 java/yaks-testing-swagger/src/test/resources/citrus-application.properties create mode 100644 java/yaks-testing-swagger/src/test/resources/cucumber.properties create mode 100644 java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/pet.json create mode 100644 java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-api.json create mode 100644 java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-open-api.feature create mode 100644 java/yaks-testing-swagger/src/test/resources/dev/yaks/testing/swagger/petstore-rest-api.feature create mode 100644 java/yaks-testing-swagger/src/test/resources/log4j2-test.xml 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");