From a4d91cdc8ae62181bbd8b3d62501c46d35fce131 Mon Sep 17 00:00:00 2001
From: Thorsten Schlathoelter
Date: Tue, 11 Jun 2024 16:01:12 +0200
Subject: [PATCH] feat(#285): support OpenAPI 3.0 from HttpOperationScenario
---
.../main/resources/swagger/petstore-api.json | 4 +-
simulator-spring-boot/pom.xml | 4 +
.../OpenApiScenarioIdGenerationMode.java | 25 +
.../SimulatorConfigurationProperties.java | 27 +-
.../simulator/http/HttpOperationScenario.java | 459 ++++--------------
.../http/HttpOperationScenarioRegistrar.java | 28 ++
.../http/HttpRequestPathScenarioMapper.java | 21 +-
.../HttpResponseActionBuilderProvider.java | 15 +
.../simulator/http/HttpScenarioGenerator.java | 153 ++++--
.../SimulatorRestConfigurationProperties.java | 68 +--
.../scenario/mapper/ScenarioMappers.java | 29 +-
.../openapi/processor/scenarioRegistrar | 2 +
.../http/HttpOperationScenarioIT.java | 244 ++++++++++
.../HttpRequestPathScenarioMapperTest.java | 135 ++++--
.../http/HttpScenarioGeneratorTest.java | 223 +++++----
.../src/test/resources/data/addPet.json | 15 +
.../test/resources/data/addPet_incorrect.json | 15 +
.../{swagger-api.json => petstore-v2.json} | 6 +-
.../test/resources/swagger/petstore-v3.json | 254 ++++++++++
19 files changed, 1075 insertions(+), 652 deletions(-)
create mode 100644 simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/OpenApiScenarioIdGenerationMode.java
create mode 100644 simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenarioRegistrar.java
create mode 100644 simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpResponseActionBuilderProvider.java
create mode 100644 simulator-spring-boot/src/main/resources/META-INF/citrus/openapi/processor/scenarioRegistrar
create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpOperationScenarioIT.java
create mode 100644 simulator-spring-boot/src/test/resources/data/addPet.json
create mode 100644 simulator-spring-boot/src/test/resources/data/addPet_incorrect.json
rename simulator-spring-boot/src/test/resources/swagger/{swagger-api.json => petstore-v2.json} (99%)
create mode 100644 simulator-spring-boot/src/test/resources/swagger/petstore-v3.json
diff --git a/simulator-samples/sample-swagger/src/main/resources/swagger/petstore-api.json b/simulator-samples/sample-swagger/src/main/resources/swagger/petstore-api.json
index 5d2e8e3d0..5fc5236b7 100644
--- a/simulator-samples/sample-swagger/src/main/resources/swagger/petstore-api.json
+++ b/simulator-samples/sample-swagger/src/main/resources/swagger/petstore-api.json
@@ -1,5 +1,5 @@
{
- "swagger": "3.0.3",
+ "swagger": "2.0",
"info": {
"description": "This is a sample server Petstore server. You can find out more about Swagger at [http://swagger.io](http://swagger.io) or on [irc.freenode.net, #swagger](http://swagger.io/irc/). For this sample, you can use the api key `special-key` to test the authorization filters.",
"version": "1.0.0",
@@ -1032,4 +1032,4 @@
"description": "Find out more about Swagger",
"url": "http://swagger.io"
}
-}
\ No newline at end of file
+}
diff --git a/simulator-spring-boot/pom.xml b/simulator-spring-boot/pom.xml
index aa94c2c5b..82889791f 100644
--- a/simulator-spring-boot/pom.xml
+++ b/simulator-spring-boot/pom.xml
@@ -130,6 +130,10 @@
org.citrusframework
citrus-http
+
+ org.citrusframework
+ citrus-openapi
+
org.citrusframework
citrus-ws
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/OpenApiScenarioIdGenerationMode.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/OpenApiScenarioIdGenerationMode.java
new file mode 100644
index 000000000..669816336
--- /dev/null
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/OpenApiScenarioIdGenerationMode.java
@@ -0,0 +1,25 @@
+package org.citrusframework.simulator.config;
+
+/**
+ * Enumeration representing the modes for generating scenario IDs in an OpenAPI context.
+ * This enumeration defines two modes:
+ *
+ * - {@link #OPERATION_ID}: Uses the operation ID defined in the OpenAPI specification.
+ * - {@link #FULL_PATH}: Uses the full path of the API endpoint.
+ *
+ * The choice of mode affects how scenario IDs are generated, with important implications:
+ *
+ * - OPERATION_ID: This mode relies on the {@code operationId} field in the OpenAPI specification, which
+ * provides a unique identifier for each operation. However, the {@code operationId} is not mandatory in the OpenAPI
+ * specification. If an {@code operationId} is not specified, this mode cannot be used effectively.
+ * - FULL_PATH: This mode constructs scenario IDs based on the entire URL path of the API endpoint, including
+ * path parameters. This is particularly useful when simulating multiple versions of the same API, as it allows for
+ * differentiation based on the endpoint path. This mode ensures unique scenario IDs even when {@code operationId}
+ * is not available or when versioning of APIs needs to be distinguished.
+ *
+ *
+ */
+public enum OpenApiScenarioIdGenerationMode {
+ FULL_PATH,
+ OPERATION_ID
+}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/SimulatorConfigurationProperties.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/SimulatorConfigurationProperties.java
index 5214fa6c9..27cc1e01e 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/SimulatorConfigurationProperties.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/SimulatorConfigurationProperties.java
@@ -16,13 +16,17 @@
package org.citrusframework.simulator.config;
+import jakarta.annotation.Nonnull;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
+import org.springframework.beans.BeansException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ApplicationContextAware;
import org.springframework.context.EnvironmentAware;
import org.springframework.core.env.Environment;
@@ -33,7 +37,7 @@
@Setter
@ToString
@ConfigurationProperties(prefix = "citrus.simulator")
-public class SimulatorConfigurationProperties implements EnvironmentAware, InitializingBean {
+public class SimulatorConfigurationProperties implements ApplicationContextAware, EnvironmentAware, InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(SimulatorConfigurationProperties.class);
@@ -46,6 +50,8 @@ public class SimulatorConfigurationProperties implements EnvironmentAware, Initi
private static final String SIMULATOR_OUTBOUND_JSON_DICTIONARY_PROPERTY = "citrus.simulator.outbound.json.dictionary.location";
private static final String SIMULATOR_OUTBOUND_JSON_DICTIONARY_ENV = "CITRUS_SIMULATOR_OUTBOUND_JSON_DICTIONARY_LOCATION";
+ private static ApplicationContext applicationContext;
+
/**
* Global option to enable/disable simulator support, default is true.
*/
@@ -104,6 +110,15 @@ public class SimulatorConfigurationProperties implements EnvironmentAware, Initi
private SimulationResults simulationResults = new SimulationResults();
+ public static ApplicationContext getApplicationContext() {
+ if (applicationContext == null) {
+ throw new IllegalStateException("Application context has not been initialized. This bean needs to be instantiated by Spring in order to function properly!");
+ }
+ return applicationContext;
+ }
+
+
+
@Override
public void setEnvironment(Environment environment) {
inboundXmlDictionary = environment.getProperty(SIMULATOR_INBOUND_XML_DICTIONARY_PROPERTY, environment.getProperty(SIMULATOR_INBOUND_XML_DICTIONARY_ENV, inboundXmlDictionary));
@@ -117,6 +132,15 @@ public void afterPropertiesSet() {
logger.info("Using the simulator configuration: {}", this);
}
+ @Override
+ public void setApplicationContext(@Nonnull ApplicationContext context) throws BeansException {
+ initStaticApplicationContext(context);
+ }
+
+ private static void initStaticApplicationContext(ApplicationContext context) {
+ applicationContext = context;
+ }
+
@Getter
@Setter
@ToString
@@ -127,4 +151,5 @@ public static class SimulationResults {
*/
private boolean resetEnabled = true;
}
+
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenario.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenario.java
index 8f4ee0b40..8334077da 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenario.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenario.java
@@ -16,395 +16,132 @@
package org.citrusframework.simulator.http;
-import io.swagger.models.ArrayModel;
-import io.swagger.models.Model;
-import io.swagger.models.Operation;
-import io.swagger.models.RefModel;
-import io.swagger.models.Response;
-import io.swagger.models.parameters.AbstractSerializableParameter;
-import io.swagger.models.parameters.BodyParameter;
-import io.swagger.models.parameters.HeaderParameter;
-import io.swagger.models.parameters.Parameter;
-import io.swagger.models.parameters.QueryParameter;
-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.citrusframework.http.actions.HttpServerRequestActionBuilder;
+import static java.lang.String.format;
+import static org.citrusframework.actions.EchoAction.Builder.echo;
+
+import io.apicurio.datamodels.openapi.models.OasDocument;
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import io.apicurio.datamodels.openapi.models.OasResponse;
+import java.util.concurrent.atomic.AtomicReference;
+import lombok.Getter;
import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
-import org.citrusframework.http.message.HttpMessageHeaders;
+import org.citrusframework.message.Message;
import org.citrusframework.message.MessageHeaders;
-import org.citrusframework.message.MessageType;
-import org.citrusframework.simulator.exception.SimulatorException;
+import org.citrusframework.openapi.OpenApiSpecification;
+import org.citrusframework.openapi.actions.OpenApiActionBuilder;
+import org.citrusframework.openapi.actions.OpenApiServerActionBuilder;
+import org.citrusframework.openapi.actions.OpenApiServerRequestActionBuilder;
+import org.citrusframework.openapi.model.OasModelHelper;
+import org.citrusframework.simulator.config.OpenApiScenarioIdGenerationMode;
import org.citrusframework.simulator.scenario.AbstractSimulatorScenario;
import org.citrusframework.simulator.scenario.ScenarioRunner;
import org.citrusframework.variable.dictionary.json.JsonPathMappingDataDictionary;
-import org.hamcrest.CustomMatcher;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
-import org.springframework.http.MediaType;
-import org.springframework.util.AntPathMatcher;
-import org.springframework.util.CollectionUtils;
-import org.springframework.util.StringUtils;
-import org.springframework.web.bind.annotation.RequestMethod;
-
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import static org.citrusframework.actions.EchoAction.Builder.echo;
+@Getter
public class HttpOperationScenario extends AbstractSimulatorScenario {
- /** Operation in wsdl */
- private final Operation operation;
-
- /** Schema model definitions */
- private final Map definitions;
+ private static final Logger logger = LoggerFactory.getLogger(HttpOperationScenario.class);
- /** Request path */
private final String path;
- /** Request method */
- private final RequestMethod method;
+ private final String scenarioId;
- /** Response */
- private Response response;
+ private final OpenApiSpecification openApiSpecification;
+
+ private final OasOperation operation;
+
+ private OasResponse response;
- /** Response status code */
private HttpStatus statusCode = HttpStatus.OK;
private JsonPathMappingDataDictionary inboundDataDictionary;
+
private JsonPathMappingDataDictionary outboundDataDictionary;
- /**
- * Default constructor.
- * @param path
- * @param method
- * @param operation
- * @param definitions
- */
- public HttpOperationScenario(String path, RequestMethod method, Operation operation, Map definitions) {
- this.operation = operation;
- this.definitions = definitions;
+ private final HttpResponseActionBuilderProvider httpResponseActionBuilderProvider;
+
+ public HttpOperationScenario(String path, String scenarioId, OpenApiSpecification openApiSpecification, OasOperation operation, HttpResponseActionBuilderProvider httpResponseActionBuilderProvider) {
this.path = path;
- this.method = method;
+ this.scenarioId = scenarioId;
+ this.openApiSpecification = openApiSpecification;
+ this.operation = operation;
+ this.httpResponseActionBuilderProvider = httpResponseActionBuilderProvider;
- if (operation.getResponses() != null) {
- this.response = operation.getResponses().get("200");
- }
+ // Note, that in case of an absent response, an OK response will be sent. This is to maintain backwards compatibility with previous swagger implementation.
+ // Also, the petstore api lacks the definition of good responses for several operations
+ this.response = OasModelHelper.getResponseForRandomGeneration(getOasDocument(), operation).orElse(null);
}
@Override
public void run(ScenarioRunner scenario) {
- scenario.name(operation.getOperationId());
- scenario.$(echo("Generated scenario from swagger operation: " + operation.getOperationId()));
-
- HttpServerRequestActionBuilder requestBuilder = switch (method) {
- case GET -> scenario.http()
- .receive()
- .get();
- case POST -> scenario.http()
- .receive()
- .post();
- case PUT -> scenario.http()
- .receive()
- .put();
- case HEAD -> scenario.http()
- .receive()
- .head();
- case DELETE -> scenario.http()
- .receive()
- .delete();
- default -> throw new SimulatorException("Unsupported request method: " + method.name());
- };
-
- requestBuilder
- .message()
- .type(MessageType.JSON)
- .header(MessageHeaders.MESSAGE_PREFIX + "generated", true)
- .header(HttpMessageHeaders.HTTP_REQUEST_URI, new CustomMatcher(String.format("request path matching %s", path)) {
- @Override
- public boolean matches(Object item) {
- return ((item instanceof String) && new AntPathMatcher().match(path, (String) item));
- }
- });
-
- if (operation.getParameters() != null) {
- operation.getParameters().stream()
- .filter(p -> p instanceof HeaderParameter)
- .filter(Parameter::getRequired)
- .forEach(p -> requestBuilder.message().header(p.getName(), createValidationExpression(((HeaderParameter) p))));
-
- String queryParams = operation.getParameters().stream()
- .filter(param -> param instanceof QueryParameter)
- .filter(Parameter::getRequired)
- .map(param -> "containsString(" + param.getName() + ")")
- .collect(Collectors.joining(", "));
-
- if (StringUtils.hasText(queryParams)) {
- requestBuilder.message().header(HttpMessageHeaders.HTTP_QUERY_PARAMS, "@assertThat(allOf(" + queryParams + "))@");
- }
-
- operation.getParameters().stream()
- .filter(p -> p instanceof BodyParameter)
- .filter(Parameter::getRequired)
- .forEach(p -> requestBuilder.message().body(createValidationPayload((BodyParameter) p)));
-
- if (inboundDataDictionary != null) {
- requestBuilder.message().dictionary(inboundDataDictionary);
- }
- }
-
- // Verify incoming request
- scenario.$(requestBuilder);
+ scenario.name(operation.operationId);
+ scenario.$(echo("Generated scenario from swagger operation: " + operation.operationId));
- HttpServerResponseActionBuilder responseBuilder = scenario.http()
- .send()
- .response(statusCode);
+ OpenApiServerActionBuilder openApiServerActionBuilder = new OpenApiActionBuilder(
+ openApiSpecification).server(getScenarioEndpoint());
- responseBuilder.message()
- .type(MessageType.JSON)
- .header(MessageHeaders.MESSAGE_PREFIX + "generated", true)
- .contentType(MediaType.APPLICATION_JSON_VALUE);
-
- if (response != null) {
- if (response.getHeaders() != null) {
- for (Map.Entry header : response.getHeaders().entrySet()) {
- responseBuilder.message().header(header.getKey(), createRandomValue(header.getValue(), false));
- }
- }
-
- if (response.getSchema() != null) {
- if (outboundDataDictionary != null &&
- (response.getSchema() instanceof RefProperty || response.getSchema() instanceof ArrayProperty)) {
- responseBuilder.message().dictionary(outboundDataDictionary);
- }
-
- responseBuilder.message().body(createRandomValue(response.getSchema(), false));
- }
- }
-
- // Return generated response
- scenario.$(responseBuilder);
+ Message receivedMessage = receive(scenario, openApiServerActionBuilder);
+ respond(scenario, openApiServerActionBuilder, receivedMessage);
}
- /**
- * Create payload from schema with random values.
- * @param property
- * @param quotes
- * @return
- */
- private String createRandomValue(Property property, 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(createRandomValue(entry.getValue(), true)).append(",");
- }
- }
-
- if (payload.toString().endsWith(",")) {
- payload.replace(payload.length() - 1, payload.length(), "");
- }
-
- payload.append("}");
- } else if (property instanceof ArrayProperty) {
- payload.append("[");
- payload.append(createRandomValue(((ArrayProperty) property).getItems(), true));
- payload.append("]");
- } 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 {
- 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("\"\"");
- } else {
- payload.append("");
- }
- }
-
- return payload.toString();
- }
+ private Message receive(ScenarioRunner scenario,
+ OpenApiServerActionBuilder openApiServerActionBuilder) {
- /**
- * Creates control payload for validation.
- * @param parameter
- * @return
- */
- private String createValidationPayload(BodyParameter parameter) {
- StringBuilder payload = new StringBuilder();
+ OpenApiServerRequestActionBuilder requestActionBuilder = openApiServerActionBuilder.receive(
+ operation.operationId);
- Model model = parameter.getSchema();
+ requestActionBuilder
+ .message()
+ .header(MessageHeaders.MESSAGE_PREFIX + "generated", true);
- if (model instanceof RefModel) {
- model = definitions.get(((RefModel) model).getSimpleRef());
+ if (operation.getParameters() != null && inboundDataDictionary != null) {
+ requestActionBuilder.message().dictionary(inboundDataDictionary);
}
- if (model instanceof ArrayModel) {
- payload.append("[");
- payload.append(createValidationExpression(((ArrayModel) model).getItems()));
- 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())).append(",");
- }
- }
+ AtomicReference receivedMessage = new AtomicReference<>();
+ requestActionBuilder.getMessageProcessors().add(
+ (message, context) -> receivedMessage.set(message));
- if (payload.toString().endsWith(",")) {
- payload.replace(payload.length() - 1, payload.length(), "");
- }
-
- payload.append("}");
- }
+ // Verify incoming request
+ scenario.$(requestActionBuilder);
- return payload.toString();
+ return receivedMessage.get();
}
- /**
- * Create validation expression using functions according to parameter type and format.
- * @param property
- * @return
- */
- private String createValidationExpression(Property property) {
- 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())).append(",");
- }
- }
-
- if (payload.toString().endsWith(",")) {
- payload.replace(payload.length() - 1, payload.length(), "");
- }
-
- payload.append("}");
- } else if (property instanceof ArrayProperty) {
- payload.append("\"@ignore@\"");
- } else if (property instanceof StringProperty) {
- 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()@\"");
- }
- } else if (property instanceof DateProperty) {
- payload.append("\"@matchesDatePattern('yyyy-MM-dd')@\"");
- } else if (property instanceof DateTimeProperty) {
- payload.append("\"@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ss')@\"");
- } else if (property instanceof IntegerProperty || property instanceof LongProperty) {
- payload.append("\"@isNumber()@\"");
- } else if (property instanceof FloatProperty || property instanceof DoubleProperty) {
- payload.append("\"@isNumber()@\"");
- } else if (property instanceof BooleanProperty) {
- payload.append("\"@matches(true|false)@\"");
- } else {
- payload.append("\"@ignore@\"");
- }
-
- return payload.toString();
- }
+ private void respond(ScenarioRunner scenario,
+ OpenApiServerActionBuilder openApiServerActionBuilder, Message receivedMessage) {
- /**
- * Create validation expression using functions according to parameter type and format.
- * @param parameter
- * @return
- */
- private 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@";
+ HttpServerResponseActionBuilder responseBuilder = null;
+ if (httpResponseActionBuilderProvider != null) {
+ responseBuilder = httpResponseActionBuilderProvider.provideHttpServerResponseActionBuilder(operation, receivedMessage);
}
- }
- /**
- * Gets the operation.
- *
- * @return
- */
- public Operation getOperation() {
- return operation;
- }
+ HttpStatus httpStatus = response != null && response.getStatusCode() != null ? HttpStatus.valueOf(Integer.parseInt(response.getStatusCode())) : HttpStatus.OK;
+ responseBuilder = responseBuilder != null ? responseBuilder : openApiServerActionBuilder.send(
+ operation.operationId, httpStatus);
- /**
- * Gets the path.
- *
- * @return
- */
- public String getPath() {
- return path;
+ responseBuilder.message()
+ .status(httpStatus)
+ .header(MessageHeaders.MESSAGE_PREFIX + "generated", true);
+
+ // Return generated response
+ scenario.$(responseBuilder);
}
/**
- * Gets the method.
+ * Gets the document.
*
* @return
*/
- public RequestMethod getMethod() {
- return method;
+ public OasDocument getOasDocument() {
+ return openApiSpecification.getOpenApiDoc(null);
}
- /**
- * Gets the response.
- *
- * @return
- */
- public Response getResponse() {
- return response;
+ public String getMethod() {
+ return operation.getMethod() != null ? operation.getMethod().toUpperCase() : null;
}
/**
@@ -412,19 +149,10 @@ public Response getResponse() {
*
* @param response
*/
- public void setResponse(Response response) {
+ public void setResponse(OasResponse response) {
this.response = response;
}
- /**
- * Gets the statusCode.
- *
- * @return
- */
- public HttpStatus getStatusCode() {
- return statusCode;
- }
-
/**
* Sets the statusCode.
*
@@ -434,15 +162,6 @@ public void setStatusCode(HttpStatus statusCode) {
this.statusCode = statusCode;
}
- /**
- * Gets the inboundDataDictionary.
- *
- * @return
- */
- public JsonPathMappingDataDictionary getInboundDataDictionary() {
- return inboundDataDictionary;
- }
-
/**
* Sets the inboundDataDictionary.
*
@@ -453,20 +172,28 @@ public void setInboundDataDictionary(JsonPathMappingDataDictionary inboundDataDi
}
/**
- * Gets the outboundDataDictionary.
+ * Sets the outboundDataDictionary.
*
- * @return
+ * @param outboundDataDictionary
*/
- public JsonPathMappingDataDictionary getOutboundDataDictionary() {
- return outboundDataDictionary;
+ public void setOutboundDataDictionary(JsonPathMappingDataDictionary outboundDataDictionary) {
+ this.outboundDataDictionary = outboundDataDictionary;
}
/**
- * Sets the outboundDataDictionary.
+ * Retrieve a unique scenario id for the oas operation.
*
- * @param outboundDataDictionary
+ * @param openApiScenarioIdGenerationMode
+ * @param path
+ * @param oasOperation
+ * @return
*/
- public void setOutboundDataDictionary(JsonPathMappingDataDictionary outboundDataDictionary) {
- this.outboundDataDictionary = outboundDataDictionary;
+ public static String getUniqueScenarioId(
+ OpenApiScenarioIdGenerationMode openApiScenarioIdGenerationMode, String path, OasOperation oasOperation) {
+
+ return switch(openApiScenarioIdGenerationMode) {
+ case OPERATION_ID -> oasOperation.operationId;
+ case FULL_PATH -> format("%s_%s", oasOperation.getMethod().toUpperCase(), path);
+ };
}
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenarioRegistrar.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenarioRegistrar.java
new file mode 100644
index 000000000..5bdc6eec3
--- /dev/null
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpOperationScenarioRegistrar.java
@@ -0,0 +1,28 @@
+package org.citrusframework.simulator.http;
+
+import org.citrusframework.openapi.OpenApiSpecification;
+import org.citrusframework.openapi.OpenApiSpecificationProcessor;
+import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+
+/**
+ * Registrar for HTTP operation scenarios based on an OpenAPI specification.
+ *
+ * This class implements the {@link OpenApiSpecificationProcessor} interface and processes an OpenAPI specification
+ * to register HTTP operation scenarios.
+ *
+ */
+public class HttpOperationScenarioRegistrar implements OpenApiSpecificationProcessor {
+
+ @Override
+ public void process(OpenApiSpecification openApiSpecification) {
+
+ HttpScenarioGenerator generator = new HttpScenarioGenerator(openApiSpecification);
+ ApplicationContext applicationContext = SimulatorConfigurationProperties.getApplicationContext();
+
+ if (applicationContext instanceof ConfigurableApplicationContext configurableApplicationContext) {
+ generator.postProcessBeanFactory(configurableApplicationContext.getBeanFactory());
+ }
+ }
+}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapper.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapper.java
index ed4c4a475..1d7c9e0d9 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapper.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapper.java
@@ -16,6 +16,7 @@
package org.citrusframework.simulator.http;
+import java.util.Objects;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.message.Message;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
@@ -44,23 +45,19 @@ public class HttpRequestPathScenarioMapper extends AbstractScenarioMapper implem
@Override
protected String getMappingKey(Message request) {
- if (request instanceof HttpMessage) {
- String requestPath = ((HttpMessage) request).getPath();
+ if (request instanceof HttpMessage httpMessage) {
+ String requestPath = httpMessage.getPath();
if (requestPath != null) {
for (HttpOperationScenario scenario : scenarioList) {
- if (scenario.getPath().equals(requestPath)) {
- if (scenario.getMethod().name().equals(((HttpMessage) request).getRequestMethod().name())) {
- return scenario.getOperation().getOperationId();
- }
+ if (Objects.equals(scenario.getMethod(), ((HttpMessage) request).getRequestMethod().name()) && Objects.equals(requestPath, scenario.getPath())) {
+ return scenario.getScenarioId();
}
}
for (HttpOperationScenario scenario : scenarioList) {
- if (pathMatcher.match(scenario.getPath(), requestPath)) {
- if (scenario.getMethod().name().equals(((HttpMessage) request).getRequestMethod().name())) {
- return scenario.getOperation().getOperationId();
- }
+ if (Objects.equals(scenario.getMethod(), ((HttpMessage) request).getRequestMethod().name()) && pathMatcher.match(scenario.getPath(), requestPath)) {
+ return scenario.getScenarioId();
}
}
}
@@ -90,8 +87,8 @@ public void setHttpScenarios(List httpScenarios) {
@Override
public void setScenarioList(List scenarioList) {
this.scenarioList = scenarioList.stream()
- .filter(scenario -> scenario instanceof HttpOperationScenario)
- .map(scenario -> (HttpOperationScenario) scenario)
+ .filter(HttpOperationScenario.class::isInstance)
+ .map(HttpOperationScenario.class::cast)
.toList();
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpResponseActionBuilderProvider.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpResponseActionBuilderProvider.java
new file mode 100644
index 000000000..a68b230b5
--- /dev/null
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpResponseActionBuilderProvider.java
@@ -0,0 +1,15 @@
+package org.citrusframework.simulator.http;
+
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
+import org.citrusframework.message.Message;
+
+/**
+ * Interface for providing an {@link HttpServerResponseActionBuilder} based on an OpenAPI operation and a received message.
+ */
+public interface HttpResponseActionBuilderProvider {
+
+ HttpServerResponseActionBuilder provideHttpServerResponseActionBuilder(OasOperation oasOperation,
+ Message receivedMessage);
+
+}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpScenarioGenerator.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpScenarioGenerator.java
index c39673a1b..d7462bbac 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpScenarioGenerator.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/HttpScenarioGenerator.java
@@ -16,17 +16,19 @@
package org.citrusframework.simulator.http;
-import static org.citrusframework.util.FileUtils.readToString;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;
-import io.swagger.models.Model;
-import io.swagger.models.Operation;
-import io.swagger.models.Path;
-import io.swagger.models.Swagger;
-import io.swagger.parser.SwaggerParser;
-import java.io.IOException;
+import io.apicurio.datamodels.combined.visitors.CombinedVisitorAdapter;
+import io.apicurio.datamodels.openapi.models.OasDocument;
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import io.apicurio.datamodels.openapi.models.OasPathItem;
+import io.apicurio.datamodels.openapi.models.OasPaths;
+import jakarta.annotation.Nonnull;
import java.util.Map;
-import org.citrusframework.simulator.exception.SimulatorException;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.openapi.OpenApiSpecification;
+import org.citrusframework.openapi.model.OasModelHelper;
+import org.citrusframework.simulator.config.OpenApiScenarioIdGenerationMode;
import org.citrusframework.spi.CitrusResourceWrapper;
import org.citrusframework.spi.Resource;
import org.slf4j.Logger;
@@ -38,7 +40,6 @@
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.util.Assert;
-import org.springframework.web.bind.annotation.RequestMethod;
/**
* @author Christoph Deppisch
@@ -48,9 +49,11 @@ public class HttpScenarioGenerator implements BeanFactoryPostProcessor {
private static final Logger logger = LoggerFactory.getLogger(HttpScenarioGenerator.class);
/**
- * Target swagger API to generate scenarios from
+ * Target Open API to generate scenarios from
*/
- private final Resource swaggerResource;
+ private final Resource openApiResource;
+
+ private OpenApiSpecification openApiSpecification;
/**
* Optional context path
@@ -61,74 +64,120 @@ public class HttpScenarioGenerator implements BeanFactoryPostProcessor {
* Constructor using Spring environment.
*/
public HttpScenarioGenerator(SimulatorRestConfigurationProperties simulatorRestConfigurationProperties) {
- swaggerResource = new CitrusResourceWrapper(
+ openApiResource = new CitrusResourceWrapper(
new PathMatchingResourcePatternResolver()
- .getResource(simulatorRestConfigurationProperties.getSwagger().getApi())
+ .getResource(simulatorRestConfigurationProperties.getOpenApi().getApi())
);
- contextPath = simulatorRestConfigurationProperties.getSwagger().getContextPath();
+ contextPath = simulatorRestConfigurationProperties.getOpenApi().getContextPath();
}
/**
* Constructor using swagger API file resource.
*
- * @param swaggerResource
+ * @param openApiResource
*/
- public HttpScenarioGenerator(Resource swaggerResource) {
- this.swaggerResource = swaggerResource;
+ public HttpScenarioGenerator(Resource openApiResource) {
+ this.openApiResource = openApiResource;
+ }
+
+ public HttpScenarioGenerator(OpenApiSpecification openApiSpecification) {
+ this.openApiResource = null;
+ this.openApiSpecification = openApiSpecification;
}
@Override
- public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
- try {
- Assert.notNull(swaggerResource,
- "Missing either swagger api system property setting or explicit swagger api resource for scenario auto generation");
+ public void postProcessBeanFactory(@Nonnull ConfigurableListableBeanFactory beanFactory) throws BeansException {
+
+ if (openApiSpecification == null) {
+ Assert.notNull(openApiResource,
+ """
+ Failed to load OpenAPI specification. No OpenAPI specification was provided.
+ To load a specification, ensure that either the 'openApiResource' property is set
+ or the 'swagger.api' system property is configured to specify the location of the OpenAPI resource.""");
+ openApiSpecification = OpenApiSpecification.from(openApiResource);
+ openApiSpecification.setRootContextPath(contextPath);
+ }
- Swagger swagger = new SwaggerParser().parse(readToString(swaggerResource));
+ HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = retrieveOptionalBuilderProvider(
+ beanFactory);
- for (Map.Entry path : swagger.getPaths().entrySet()) {
- for (Map.Entry operation : path.getValue().getOperationMap().entrySet()) {
+ OpenApiScenarioIdGenerationMode openApiScenarioIdGenerationMode = beanFactory.getBean(
+ SimulatorRestConfigurationProperties.class).getOpenApiScenarioIdGenerationMode();
- if (beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry) {
- logger.info("Register auto generated scenario as bean definition: {}", operation.getValue().getOperationId());
+ TestContext testContext = new TestContext();
+ OasDocument openApiDocument = openApiSpecification.getOpenApiDoc(testContext);
+ if (openApiDocument != null && openApiDocument.paths != null) {
+ openApiDocument.paths.accept(new CombinedVisitorAdapter() {
- BeanDefinitionBuilder beanDefinitionBuilder = genericBeanDefinition(HttpOperationScenario.class)
- .addConstructorArgValue((contextPath + (swagger.getBasePath() != null ? swagger.getBasePath() : "")) + path.getKey())
- .addConstructorArgValue(RequestMethod.valueOf(operation.getKey().name()))
- .addConstructorArgValue(operation.getValue())
- .addConstructorArgValue(swagger.getDefinitions());
+ @Override
+ public void visitPaths(OasPaths oasPaths) {
+ oasPaths.getPathItems().forEach(oasPathItem -> oasPathItem.accept(this));
+ }
- if (beanFactory.containsBeanDefinition("inboundJsonDataDictionary")) {
- beanDefinitionBuilder.addPropertyReference("inboundDataDictionary", "inboundJsonDataDictionary");
- }
+ @Override
+ public void visitPathItem(OasPathItem oasPathItem) {
+ String path = oasPathItem.getPath();
+ for (Map.Entry operationEntry : OasModelHelper.getOperationMap(
+ oasPathItem).entrySet()) {
- if (beanFactory.containsBeanDefinition("outboundJsonDataDictionary")) {
- beanDefinitionBuilder.addPropertyReference("outboundDataDictionary", "outboundJsonDataDictionary");
- }
+ String fullPath = contextPath + OasModelHelper.getBasePath(openApiDocument) + path;
+ OasOperation oasOperation = operationEntry.getValue();
+
+ String scenarioId = HttpOperationScenario.getUniqueScenarioId(openApiScenarioIdGenerationMode, OasModelHelper.getBasePath(openApiDocument) + path, oasOperation);
+
+ if (beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry) {
+ logger.info("Register auto generated scenario as bean definition: {}", fullPath);
- beanDefinitionRegistry.registerBeanDefinition(operation.getValue().getOperationId(), beanDefinitionBuilder.getBeanDefinition());
- } else {
- logger.info("Register auto generated scenario as singleton: {}", operation.getValue().getOperationId());
- beanFactory.registerSingleton(operation.getValue().getOperationId(), createScenario((contextPath + (swagger.getBasePath() != null ? swagger.getBasePath() : "")) + path.getKey(), RequestMethod.valueOf(operation.getKey().name()), operation.getValue(), swagger.getDefinitions()));
+ BeanDefinitionBuilder beanDefinitionBuilder = genericBeanDefinition(HttpOperationScenario.class)
+ .addConstructorArgValue(fullPath)
+ .addConstructorArgValue(scenarioId)
+ .addConstructorArgValue(openApiSpecification)
+ .addConstructorArgValue(oasOperation)
+ .addConstructorArgValue(httpResponseActionBuilderProvider);
+
+ if (beanFactory.containsBeanDefinition("inboundJsonDataDictionary")) {
+ beanDefinitionBuilder.addPropertyReference("inboundDataDictionary", "inboundJsonDataDictionary");
+ }
+
+ if (beanFactory.containsBeanDefinition("outboundJsonDataDictionary")) {
+ beanDefinitionBuilder.addPropertyReference("outboundDataDictionary", "outboundJsonDataDictionary");
+ }
+
+ beanDefinitionRegistry.registerBeanDefinition(scenarioId, beanDefinitionBuilder.getBeanDefinition());
+ } else {
+ logger.info("Register auto generated scenario as singleton: {}", scenarioId);
+ beanFactory.registerSingleton(scenarioId, createScenario(fullPath, scenarioId, openApiSpecification, oasOperation, httpResponseActionBuilderProvider));
+ }
}
}
- }
- } catch (IOException e) {
- throw new SimulatorException("Failed to read swagger api resource", e);
+ });
+ }
+ }
+
+ private static HttpResponseActionBuilderProvider retrieveOptionalBuilderProvider(
+ ConfigurableListableBeanFactory beanFactory) {
+ HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = null;
+ try {
+ httpResponseActionBuilderProvider = beanFactory.getBean(
+ HttpResponseActionBuilderProvider.class);
+ } catch (BeansException e) {
+ // Ignore non existing optional provider
}
+ return httpResponseActionBuilderProvider;
}
/**
* Creates an HTTP scenario based on the given swagger path and operation information.
*
- * @param path Request path
- * @param method Request method
- * @param operation Swagger operation
- * @param definitions Additional definitions
+ * @param path Full request path, including the context
+ * @param scenarioId Request method
+ * @param openApiSpecification OpenApiSpecification
+ * @param operation OpenApi operation
* @return a matching HTTP scenario
*/
- protected HttpOperationScenario createScenario(String path, RequestMethod method, Operation operation, Map definitions) {
- return new HttpOperationScenario(path, method, operation, definitions);
+ protected HttpOperationScenario createScenario(String path, String scenarioId, OpenApiSpecification openApiSpecification, OasOperation operation, HttpResponseActionBuilderProvider httpResponseActionBuilderProvider) {
+ return new HttpOperationScenario(path, scenarioId, openApiSpecification, operation, httpResponseActionBuilderProvider);
}
public String getContextPath() {
@@ -137,5 +186,9 @@ public String getContextPath() {
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
+
+ if (openApiSpecification != null) {
+ openApiSpecification.setRootContextPath(contextPath);
+ }
}
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestConfigurationProperties.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestConfigurationProperties.java
index e962dd213..eeaeffd73 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestConfigurationProperties.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestConfigurationProperties.java
@@ -16,21 +16,25 @@
package org.citrusframework.simulator.http;
+import static java.util.Collections.emptyList;
+import static java.util.Collections.unmodifiableList;
+
import jakarta.validation.constraints.NotNull;
+import java.util.List;
+import lombok.Getter;
+import lombok.Setter;
import org.apache.commons.lang3.builder.ToStringBuilder;
+import org.citrusframework.simulator.config.OpenApiScenarioIdGenerationMode;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
-import java.util.List;
-
-import static java.util.Collections.emptyList;
-import static java.util.Collections.unmodifiableList;
-
/**
* @author Christoph Deppisch
*/
+@Getter
+@Setter
@ConfigurationProperties(prefix = "citrus.simulator.rest")
public class SimulatorRestConfigurationProperties implements InitializingBean {
@@ -47,25 +51,15 @@ public class SimulatorRestConfigurationProperties implements InitializingBean {
*/
private List urlMappings = List.of("/services/rest/**");
- private Swagger swagger = new Swagger();
-
/**
- * Gets the enabled.
- *
- * @return
+ * The scenario id generation mode for open api scenario generation.
*/
- public boolean isEnabled() {
- return enabled;
- }
+ private OpenApiScenarioIdGenerationMode openApiScenarioIdGenerationMode = OpenApiScenarioIdGenerationMode.FULL_PATH;
/**
- * Sets the enabled.
- *
- * @param enabled
+ * The OpenApi used by the simulator to simulate OpenApi operations.
*/
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
+ private OpenApi openApi = new OpenApi();
/**
* Gets the urlMappings.
@@ -86,14 +80,6 @@ public void setUrlMappings(List urlMappings) {
this.urlMappings = urlMappings != null ? unmodifiableList(urlMappings) : emptyList();
}
- public Swagger getSwagger() {
- return swagger;
- }
-
- public void setSwagger(Swagger swagger) {
- this.swagger = swagger;
- }
-
@Override
public void afterPropertiesSet() throws Exception {
logger.info("Using the simulator configuration: {}", this);
@@ -107,34 +93,12 @@ public String toString() {
.toString();
}
- public static class Swagger {
+ @Getter
+ @Setter
+ public static class OpenApi {
private String api;
private String contextPath;
private boolean enabled = false;
-
- public String getApi() {
- return api;
- }
-
- public void setApi(String api) {
- this.api = api;
- }
-
- public String getContextPath() {
- return contextPath;
- }
-
- public void setContextPath(String contextPath) {
- this.contextPath = contextPath;
- }
-
- public boolean isEnabled() {
- return enabled;
- }
-
- public void setEnabled(boolean enabled) {
- this.enabled = enabled;
- }
}
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappers.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappers.java
index b0edaa50e..1727deb47 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappers.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappers.java
@@ -16,7 +16,10 @@
package org.citrusframework.simulator.scenario.mapper;
-import io.swagger.models.Operation;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
import org.citrusframework.message.Message;
import org.citrusframework.simulator.config.SimulatorConfigurationPropertiesAware;
import org.citrusframework.simulator.http.HttpOperationScenario;
@@ -28,11 +31,6 @@
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.StringUtils;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Optional;
-
/**
* Scenario mapper chain goes through a list of mappers to find best match of extracted mapping keys. When no suitable
* mapping key is found in the list of mappers a default mapping is used based on provided base class evaluation.
@@ -67,8 +65,8 @@ public static ScenarioMappers of(ScenarioMapper ... scenarioMappers) {
public String getMappingKey(Message message) {
return scenarioMapperList.stream()
.map(mapper -> {
- if (mapper instanceof AbstractScenarioMapper) {
- ((AbstractScenarioMapper) mapper).setUseDefaultMapping(false);
+ if (mapper instanceof AbstractScenarioMapper abstractScenarioMapper) {
+ abstractScenarioMapper.setUseDefaultMapping(false);
}
try {
@@ -82,11 +80,8 @@ public String getMappingKey(Message message) {
.filter(StringUtils::hasLength)
.filter(key -> scenarioList.parallelStream()
.anyMatch(scenario -> {
- if (scenario instanceof HttpOperationScenario) {
- return Optional.ofNullable(((HttpOperationScenario) scenario).getOperation())
- .map(Operation::getOperationId)
- .orElse("")
- .equals(key);
+ if (scenario instanceof HttpOperationScenario httpOperationScenario) {
+ return key.equals(httpOperationScenario.getScenarioId());
}
return Optional.ofNullable(AnnotationUtils.findAnnotation(scenario.getClass(), Scenario.class))
@@ -101,13 +96,13 @@ public String getMappingKey(Message message) {
@Override
public void afterPropertiesSet() {
scenarioMapperList.stream()
- .filter(mapper -> mapper instanceof ScenarioListAware)
- .map(mapper -> (ScenarioListAware) mapper)
+ .filter(ScenarioListAware.class::isInstance)
+ .map(ScenarioListAware.class::cast)
.forEach(mapper -> mapper.setScenarioList(scenarioList));
scenarioMapperList.stream()
- .filter(mapper -> mapper instanceof SimulatorConfigurationPropertiesAware)
- .map(mapper -> (SimulatorConfigurationPropertiesAware) mapper)
+ .filter(SimulatorConfigurationPropertiesAware.class::isInstance)
+ .map(SimulatorConfigurationPropertiesAware.class::cast)
.forEach(mapper -> mapper.setSimulatorConfigurationProperties(getSimulatorConfigurationProperties()));
}
diff --git a/simulator-spring-boot/src/main/resources/META-INF/citrus/openapi/processor/scenarioRegistrar b/simulator-spring-boot/src/main/resources/META-INF/citrus/openapi/processor/scenarioRegistrar
new file mode 100644
index 000000000..4c6b54b46
--- /dev/null
+++ b/simulator-spring-boot/src/main/resources/META-INF/citrus/openapi/processor/scenarioRegistrar
@@ -0,0 +1,2 @@
+name=httpOperationScenarioRegistrar
+type=org.citrusframework.simulator.http.HttpOperationScenarioRegistrar
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpOperationScenarioIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpOperationScenarioIT.java
new file mode 100644
index 000000000..91f12d81b
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpOperationScenarioIT.java
@@ -0,0 +1,244 @@
+package org.citrusframework.simulator.http;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatCode;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import java.io.IOException;
+import java.util.List;
+import java.util.function.Function;
+import java.util.stream.Stream;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.exceptions.TestCaseFailedException;
+import org.citrusframework.functions.DefaultFunctionRegistry;
+import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
+import org.citrusframework.http.message.HttpMessage;
+import org.citrusframework.log.DefaultLogModifier;
+import org.citrusframework.message.Message;
+import org.citrusframework.openapi.OpenApiRepository;
+import org.citrusframework.openapi.actions.OpenApiClientResponseActionBuilder;
+import org.citrusframework.openapi.model.OasModelHelper;
+import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.citrusframework.simulator.scenario.ScenarioEndpoint;
+import org.citrusframework.simulator.scenario.ScenarioEndpointConfiguration;
+import org.citrusframework.simulator.scenario.ScenarioRunner;
+import org.citrusframework.spi.Resources.ClasspathResource;
+import org.citrusframework.util.FileUtils;
+import org.citrusframework.validation.DefaultMessageHeaderValidator;
+import org.citrusframework.validation.DefaultMessageValidatorRegistry;
+import org.citrusframework.validation.context.HeaderValidationContext;
+import org.citrusframework.validation.json.JsonMessageValidationContext;
+import org.citrusframework.validation.json.JsonTextMessageValidator;
+import org.citrusframework.validation.matcher.DefaultValidationMatcherRegistry;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.http.HttpHeaders;
+import org.springframework.test.util.ReflectionTestUtils;
+
+@ExtendWith(MockitoExtension.class)
+class HttpOperationScenarioIT {
+
+ private static final Function IDENTITY = (text) -> text;
+
+ private final DirectScenarioEndpoint scenarioEndpoint = new DirectScenarioEndpoint();
+
+ private static final OpenApiRepository openApiRepository = new OpenApiRepository();
+
+ private static DefaultListableBeanFactory defaultListableBeanFactory;
+
+ private ScenarioRunner scenarioRunner;
+
+ private TestContext testContext;
+
+ @BeforeAll
+ static void beforeAll() {
+ ConfigurableApplicationContext applicationContext = mock();
+ defaultListableBeanFactory = new DefaultListableBeanFactory();
+ doReturn(defaultListableBeanFactory).when(applicationContext).getBeanFactory();
+ SimulatorConfigurationProperties simulatorConfigurationProperties = new SimulatorConfigurationProperties();
+ simulatorConfigurationProperties.setApplicationContext(applicationContext);
+
+ defaultListableBeanFactory.registerSingleton("SimulatorRestConfigurationProperties", new SimulatorRestConfigurationProperties());
+
+ openApiRepository.addRepository(new ClasspathResource("swagger/petstore-v2.json"));
+ openApiRepository.addRepository(new ClasspathResource("swagger/petstore-v3.json"));
+ }
+
+ @BeforeEach
+ void beforeEach() {
+ testContext = new TestContext();
+ testContext.setReferenceResolver(mock());
+ testContext.setMessageValidatorRegistry(new DefaultMessageValidatorRegistry());
+ testContext.setFunctionRegistry(new DefaultFunctionRegistry());
+ testContext.setValidationMatcherRegistry(new DefaultValidationMatcherRegistry());
+ testContext.setLogModifier(new DefaultLogModifier());
+ scenarioRunner = new ScenarioRunner(scenarioEndpoint, mock(), testContext);
+ }
+
+ static Stream scenarioExecution() {
+ return Stream.of(
+ arguments("v2_addPet_success", "POST_/petstore/v2/pet", "data/addPet.json", IDENTITY, null),
+ arguments("v3_addPet_success", "POST_/petstore/v3/pet", "data/addPet.json", IDENTITY, null),
+ arguments("v2_addPet_payloadValidationFailure", "POST_/petstore/v2/pet", "data/addPet_incorrect.json", IDENTITY, "Missing JSON entry, expected 'id' to be in '[photoUrls, wrong_id_property, name, category, tags, status]'"),
+ arguments("v3_addPet_payloadValidationFailure", "POST_/petstore/v3/pet", "data/addPet_incorrect.json", IDENTITY, "Missing JSON entry, expected 'id' to be in '[photoUrls, wrong_id_property, name, category, tags, status]'"),
+ arguments("v2_getPetById_success", "GET_/petstore/v2/pet/{petId}", null, (Function)(text) -> text.replace("{petId}", "1234"), null),
+ arguments("v3_getPetById_success", "GET_/petstore/v3/pet/{petId}", null, (Function)(text) -> text.replace("{petId}", "1234"), null),
+ arguments("v2_getPetById_pathParameterValidationFailure", "GET_/petstore/v2/pet/{petId}", null, (Function)(text) -> text.replace("{petId}", "xxxx"), "MatchesValidationMatcher failed for field 'citrus_http_request_uri'. Received value is '/petstore/v2/pet/xxxx', control value is '/petstore/v2/pet/[0-9]+'"),
+ arguments("v3_getPetById_pathParameterValidationFailure", "GET_/petstore/v3/pet/{petId}", null, (Function)(text) -> text.replace("{petId}", "xxxx"), "MatchesValidationMatcher failed for field 'citrus_http_request_uri'. Received value is '/petstore/v3/pet/xxxx', control value is '/petstore/v3/pet/[0-9]+'")
+ );
+ }
+
+ @ParameterizedTest(name="{0}")
+ @MethodSource()
+ void scenarioExecution(String name, String operationName, String payloadFile, Function urlAdjuster, String exceptionMessage)
+ throws IOException {
+ if (defaultListableBeanFactory.containsSingleton("httpResponseActionBuilderProvider")) {
+ defaultListableBeanFactory.destroySingleton("httpResponseActionBuilderProvider");
+ }
+
+ HttpOperationScenario httpOperationScenario = getHttpOperationScenario(operationName);
+ HttpMessage controlMessage = new HttpMessage();
+ OpenApiClientResponseActionBuilder.fillMessageFromResponse(httpOperationScenario.getOpenApiSpecification(), testContext, controlMessage, httpOperationScenario.getOperation(), httpOperationScenario.getResponse());
+
+ this.scenarioExecution(operationName, payloadFile, urlAdjuster, exceptionMessage, controlMessage);
+ }
+
+ @ParameterizedTest(name="{0}_custom_payload")
+ @MethodSource("scenarioExecution")
+ void scenarioExecutionWithProvider(String name, String operationName, String payloadFile, Function urlAdjuster, String exceptionMessage) {
+
+ String payload = "{\"id\":1234}";
+ HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = (oasOperation, receivedMessage) -> {
+ HttpServerResponseActionBuilder serverResponseActionBuilder = new HttpServerResponseActionBuilder();
+ serverResponseActionBuilder
+ .endpoint(scenarioEndpoint)
+ .getMessageBuilderSupport()
+ .body(payload);
+ return serverResponseActionBuilder;
+ };
+
+ if (!defaultListableBeanFactory.containsSingleton("httpResponseActionBuilderProvider")) {
+ defaultListableBeanFactory.registerSingleton("httpResponseActionBuilderProvider",
+ httpResponseActionBuilderProvider);
+ }
+
+ HttpOperationScenario httpOperationScenario = getHttpOperationScenario(operationName);
+ try {
+ ReflectionTestUtils.setField(httpOperationScenario, "httpResponseActionBuilderProvider",
+ httpResponseActionBuilderProvider);
+
+ HttpMessage correctPayloadMessage = new HttpMessage(payload);
+ assertThatCode(() -> this.scenarioExecution(operationName, payloadFile, urlAdjuster, exceptionMessage,
+ correctPayloadMessage)).doesNotThrowAnyException();
+
+ if (exceptionMessage == null) {
+ String otherPayload = "{\"id\":12345}";
+ HttpMessage incorrectPayloadMessage = new HttpMessage(otherPayload);
+ assertThatThrownBy(
+ () -> this.scenarioExecution(operationName, payloadFile, urlAdjuster,
+ exceptionMessage,
+ incorrectPayloadMessage)).isInstanceOf(CitrusRuntimeException.class);
+ }
+ } finally {
+ ReflectionTestUtils.setField(httpOperationScenario, "httpResponseActionBuilderProvider",
+ null);
+ }
+ }
+
+ private void scenarioExecution(String operationName, String payloadFile, Function urlAdjuster, String exceptionMessage, Message controlMessage)
+ throws IOException {
+ HttpOperationScenario httpOperationScenario = getHttpOperationScenario(operationName);
+ OasOperation oasOperation = httpOperationScenario.getOperation();
+
+ String payload = payloadFile != null ? FileUtils.readToString(new ClasspathResource(payloadFile)) : null;
+
+ Message receiveMessage = new HttpMessage()
+ .setPayload(payload)
+ .setHeader("citrus_http_request_uri", urlAdjuster.apply(httpOperationScenario.getPath()))
+ .setHeader("citrus_http_method", httpOperationScenario.getMethod().toUpperCase());
+
+ OasModelHelper.getRequestContentType(oasOperation)
+ .ifPresent(contentType -> receiveMessage.setHeader(HttpHeaders.CONTENT_TYPE, contentType));
+
+ scenarioEndpoint.setReceiveMessage(receiveMessage);
+
+ ReflectionTestUtils.setField(httpOperationScenario, "scenarioEndpoint",
+ scenarioEndpoint);
+
+ if (exceptionMessage != null) {
+ assertThatThrownBy(() -> httpOperationScenario.run(scenarioRunner)).isInstanceOf(
+ TestCaseFailedException.class).hasMessage(exceptionMessage);
+ } else {
+ assertThatCode(() -> httpOperationScenario.run(scenarioRunner)).doesNotThrowAnyException();
+
+ Message sendMessage = scenarioEndpoint.getSendMessage();
+
+ JsonTextMessageValidator jsonTextMessageValidator = new JsonTextMessageValidator();
+ jsonTextMessageValidator.validateMessage(sendMessage, controlMessage, testContext,
+ List.of(new JsonMessageValidationContext()));
+ DefaultMessageHeaderValidator defaultMessageHeaderValidator = new DefaultMessageHeaderValidator();
+ defaultMessageHeaderValidator.validateMessage(sendMessage, controlMessage, testContext, List.of(new HeaderValidationContext()));
+ }
+
+ }
+
+ private HttpOperationScenario getHttpOperationScenario(String operationName) {
+ Object bean = defaultListableBeanFactory.getBean(operationName);
+
+ assertThat(bean).isInstanceOf(HttpOperationScenario.class);
+
+ return (HttpOperationScenario) bean;
+ }
+
+ private static class DirectScenarioEndpoint extends ScenarioEndpoint {
+
+ private Message receiveMessage;
+
+ private Message sendMessage;
+
+ public DirectScenarioEndpoint() {
+ super(new ScenarioEndpointConfiguration());
+ }
+
+ @Override
+ public void send(Message message, TestContext context) {
+ this.sendMessage = new HttpMessage(message);
+
+ if (sendMessage.getPayload() instanceof String stringPayload) {
+ this.sendMessage.setPayload(
+ context.replaceDynamicContentInString(stringPayload));
+ }
+ }
+
+ @Override
+ public Message receive(TestContext context) {
+ return receiveMessage;
+ }
+
+ @Override
+ public Message receive(TestContext context, long timeout) {
+ return receiveMessage;
+ }
+
+ public void setReceiveMessage(Message receiveMessage) {
+ this.receiveMessage = receiveMessage;
+ }
+
+ public Message getSendMessage() {
+ return sendMessage;
+ }
+
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapperTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapperTest.java
index 6d4c69e54..41682e562 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapperTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestPathScenarioMapperTest.java
@@ -16,33 +16,46 @@
package org.citrusframework.simulator.http;
-import io.swagger.models.Operation;
+import static org.assertj.core.api.Assertions.fail;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+
+import io.apicurio.datamodels.openapi.models.OasDocument;
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import io.apicurio.datamodels.openapi.v2.models.Oas20Document;
+import io.apicurio.datamodels.openapi.v2.models.Oas20Operation;
+import io.apicurio.datamodels.openapi.v3.models.Oas30Document;
+import io.apicurio.datamodels.openapi.v3.models.Oas30Operation;
+import java.util.Arrays;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.http.message.HttpMessage;
+import org.citrusframework.openapi.OpenApiSpecification;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.Mock;
-import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.http.HttpMethod;
import org.springframework.web.bind.annotation.RequestMethod;
-import java.util.Arrays;
-import java.util.Collections;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.when;
-
/**
* @author Christoph Deppisch
*/
@ExtendWith(MockitoExtension.class)
class HttpRequestPathScenarioMapperTest {
+ public static final String DEFAULT_SCENARIO = "default";
+ public static final String FOO_LIST_SCENARIO = "fooListScenario";
+ public static final String FOO_LIST_POST_SCENARIO = "fooListPostScenario";
+ public static final String BAR_LIST_SCENARIO = "barListScenario";
+ public static final String FOO_SCENARIO = "fooScenario";
+ public static final String BAR_SCENARIO = "barScenario";
+ public static final String FOO_DETAIL_SCENARIO = "fooDetailScenario";
+ public static final String BAR_DETAIL_SCENARIO = "barDetailScenario";
@Mock
private SimulatorConfigurationProperties simulatorConfigurationMock;
@@ -53,42 +66,60 @@ void beforeEachSetup() {
fixture = new HttpRequestPathScenarioMapper();
fixture.setConfiguration(simulatorConfigurationMock);
- doReturn("default").when(simulatorConfigurationMock).getDefaultScenario();
+ doReturn(DEFAULT_SCENARIO).when(simulatorConfigurationMock).getDefaultScenario();
}
- @Test
- void testGetMappingKey() {
- Operation operation = Mockito.mock(Operation.class);
-
- fixture.setScenarioList(Arrays.asList(new HttpOperationScenario("/issues/foos", RequestMethod.GET, operation, Collections.emptyMap()),
- new HttpOperationScenario("/issues/foos", RequestMethod.POST, operation, Collections.emptyMap()),
- new HttpOperationScenario("/issues/foo/{id}", RequestMethod.GET, operation, Collections.emptyMap()),
- new HttpOperationScenario("/issues/foo/detail", RequestMethod.GET, operation, Collections.emptyMap()),
- new HttpOperationScenario("/issues/bars", RequestMethod.GET, operation, Collections.emptyMap()),
- new HttpOperationScenario("/issues/bar/{id}", RequestMethod.GET, operation, Collections.emptyMap()),
- new HttpOperationScenario("/issues/bar/detail", RequestMethod.GET, operation, Collections.emptyMap())));
-
- when(operation.getOperationId())
- .thenReturn("fooListScenario")
- .thenReturn("fooListPostScenario")
- .thenReturn("barListScenario")
- .thenReturn("fooScenario")
- .thenReturn("barScenario")
- .thenReturn("fooDetailScenario")
- .thenReturn("barDetailScenario");
-
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET)), "default");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.POST)), "default");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues")), "default");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/foos")), "fooListScenario");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.POST).path("/issues/foos")), "fooListPostScenario");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.PUT).path("/issues/foos")), "default");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/bars")), "barListScenario");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.DELETE).path("/issues/bars")), "default");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/foo/1")), "fooScenario");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/bar/1")), "barScenario");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/foo/detail")), "fooDetailScenario");
- assertEquals(fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/bar/detail")), "barDetailScenario");
+ @ParameterizedTest
+ @ValueSource(strings = {"oas2", "oas3"})
+ void testGetMappingKey(String version) {
+ OpenApiSpecification openApiSpecificationMock = mock();
+
+ OasDocument oasDocument = null;
+ if ("oas2".equals(version)) {
+ oasDocument = mock(Oas20Document.class);
+ } else if ("oas3".equals(version)) {
+ oasDocument = mock(Oas30Document.class);
+ } else {
+ fail("Unexpected version: "+ version);
+ }
+
+ doReturn(oasDocument).when(openApiSpecificationMock).getOpenApiDoc(null);
+
+ fixture.setScenarioList(Arrays.asList(new HttpOperationScenario("/issues/foos",
+ FOO_LIST_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.GET), null),
+ new HttpOperationScenario("/issues/foos", FOO_LIST_POST_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.POST), null),
+ new HttpOperationScenario("/issues/foo/{id}", FOO_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.GET), null),
+ new HttpOperationScenario("/issues/foo/detail", FOO_DETAIL_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.GET), null),
+ new HttpOperationScenario("/issues/bars", BAR_LIST_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.GET), null),
+ new HttpOperationScenario("/issues/bar/{id}", BAR_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.GET), null),
+ new HttpOperationScenario("/issues/bar/detail", BAR_DETAIL_SCENARIO, openApiSpecificationMock, mockOperation(oasDocument, RequestMethod.GET), null)));
+
+ assertEquals(DEFAULT_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET)));
+ assertEquals(DEFAULT_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.POST)));
+ assertEquals(DEFAULT_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues")));
+ assertEquals(FOO_LIST_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/foos")));
+ assertEquals(FOO_LIST_POST_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.POST).path("/issues/foos")));
+ assertEquals(DEFAULT_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.PUT).path("/issues/foos")));
+ assertEquals(BAR_LIST_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/bars")));
+ assertEquals(DEFAULT_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.DELETE).path("/issues/bars")));
+ assertEquals(FOO_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/foo/1")));
+ assertEquals(BAR_SCENARIO,
+ fixture.getMappingKey(new HttpMessage().method(HttpMethod.GET).path("/issues/bar/1")));
+ assertEquals(FOO_DETAIL_SCENARIO,
+ fixture.getMappingKey(
+ new HttpMessage().method(HttpMethod.GET).path("/issues/foo/detail")));
+ assertEquals(BAR_DETAIL_SCENARIO,
+ fixture.getMappingKey(
+ new HttpMessage().method(HttpMethod.GET).path("/issues/bar/detail")));
fixture.setUseDefaultMapping(false);
@@ -98,4 +129,18 @@ void testGetMappingKey() {
HttpMessage httpGetIssuesMessage = new HttpMessage().method(HttpMethod.GET).path("/issues");
assertThrows(CitrusRuntimeException.class, () -> fixture.getMappingKey(httpGetIssuesMessage));
}
+
+ private OasOperation mockOperation(OasDocument oasDocument, RequestMethod requestMethod) {
+
+ OasOperation oasOperationMock = null;
+ if (oasDocument instanceof Oas20Document) {
+ oasOperationMock = mock(Oas20Operation.class);
+ } else if (oasDocument instanceof Oas30Document) {
+ oasOperationMock = mock(Oas30Operation.class);
+ } else {
+ fail("Unexpected version document type!");
+ }
+ doReturn(requestMethod.toString()).when(oasOperationMock).getMethod();
+ return oasOperationMock;
+ }
}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpScenarioGeneratorTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpScenarioGeneratorTest.java
index a13140bd7..553c4bb57 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpScenarioGeneratorTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpScenarioGeneratorTest.java
@@ -16,26 +16,28 @@
package org.citrusframework.simulator.http;
-import io.swagger.models.Operation;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import org.citrusframework.openapi.OpenApiSpecification;
import org.citrusframework.spi.Resources;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.Mock;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
-import org.springframework.web.bind.annotation.RequestMethod;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNotNull;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.doAnswer;
-import static org.mockito.Mockito.doReturn;
-import static org.mockito.Mockito.verify;
/**
* @author Christoph Deppisch
@@ -43,148 +45,161 @@
@ExtendWith(MockitoExtension.class)
class HttpScenarioGeneratorTest {
- @Mock
- private ConfigurableListableBeanFactory beanFactoryMock;
+ private HttpScenarioGenerator fixture;
- @Mock
- private DefaultListableBeanFactory beanRegistryMock;
+ @ParameterizedTest
+ @ValueSource(strings={"v2", "v3"})
+ void generateHttpScenarios(String version) {
+ ConfigurableListableBeanFactory beanFactoryMock = mock();
- private HttpScenarioGenerator fixture;
+ mockBeanFactory(beanFactoryMock);
- @BeforeEach
- void beforeEachSetup() {
- fixture = new HttpScenarioGenerator(new Resources.ClasspathResource("swagger/swagger-api.json"));
- }
+ fixture = new HttpScenarioGenerator(new Resources.ClasspathResource(
+ "swagger/petstore-"+version+".json"));
+
+ String addPetScenarioId = "POST_/petstore/"+version+"/pet";
+ String getPetScenarioId = "GET_/petstore/"+version+"/pet/{petId}";
+ String deletePetScenarioId = "DELETE_/petstore/"+version+"/pet/{petId}";
- @Test
- void generateHttpScenarios() {
+ String context = "/petstore/"+ version ;
doAnswer(invocation -> {
HttpOperationScenario scenario = (HttpOperationScenario) invocation.getArguments()[1];
-
- assertNotNull(scenario.getOperation());
- assertEquals(scenario.getPath(), "/v2/pet");
- assertEquals(scenario.getMethod(), RequestMethod.POST);
-
+ assertScenarioProperties(scenario, context+"/pet", addPetScenarioId, "POST");
return null;
- }).when(beanFactoryMock).registerSingleton(eq("addPet"), any(HttpOperationScenario.class));
+ }).when(beanFactoryMock).registerSingleton(eq(addPetScenarioId), any(HttpOperationScenario.class));
doAnswer(invocation -> {
HttpOperationScenario scenario = (HttpOperationScenario) invocation.getArguments()[1];
-
- assertNotNull(scenario.getOperation());
- assertEquals(scenario.getPath(), "/v2/pet/{petId}");
- assertEquals(scenario.getMethod(), RequestMethod.GET);
-
+ assertScenarioProperties(scenario, context+"/pet/{petId}", getPetScenarioId, "GET");
return null;
- }).when(beanFactoryMock).registerSingleton(eq("getPetById"), any(HttpOperationScenario.class));
+ }).when(beanFactoryMock).registerSingleton(eq(getPetScenarioId), any(HttpOperationScenario.class));
doAnswer(invocation -> {
HttpOperationScenario scenario = (HttpOperationScenario) invocation.getArguments()[1];
-
- assertNotNull(scenario.getOperation());
- assertEquals(scenario.getPath(), "/v2/pet/{petId}");
- assertEquals(scenario.getMethod(), RequestMethod.DELETE);
-
+ assertScenarioProperties(scenario, context+"/pet/{petId}", deletePetScenarioId, "DELETE");
return null;
- }).when(beanFactoryMock).registerSingleton(eq("deletePet"), any(HttpOperationScenario.class));
+ }).when(beanFactoryMock).registerSingleton(eq(deletePetScenarioId), any(HttpOperationScenario.class));
fixture.postProcessBeanFactory(beanFactoryMock);
- verify(beanFactoryMock).registerSingleton(eq("addPet"), any(HttpOperationScenario.class));
- verify(beanFactoryMock).registerSingleton(eq("getPetById"), any(HttpOperationScenario.class));
- verify(beanFactoryMock).registerSingleton(eq("deletePet"), any(HttpOperationScenario.class));
+ verify(beanFactoryMock).registerSingleton(eq(addPetScenarioId), any(HttpOperationScenario.class));
+ verify(beanFactoryMock).registerSingleton(eq(getPetScenarioId), any(HttpOperationScenario.class));
+ verify(beanFactoryMock).registerSingleton(eq(deletePetScenarioId), any(HttpOperationScenario.class));
}
- @Test
- void testGenerateScenariosWithBeandDefinitionRegistry() {
- doAnswer(invocation -> {
- BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
+ @ParameterizedTest
+ @ValueSource(strings={"v2", "v3"})
+ void testGenerateScenariosWithBeanDefinitionRegistry(String version) {
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(0, String.class).getValue(), "/v2/pet");
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(1, RequestMethod.class).getValue(), RequestMethod.POST);
- assertNotNull(scenario.getConstructorArgumentValues().getArgumentValue(2, Operation.class).getValue());
- assertNull(scenario.getPropertyValues().get("inboundDataDictionary"));
- assertNull(scenario.getPropertyValues().get("outboundDataDictionary"));
+ DefaultListableBeanFactory beanRegistryMock = mock();
+ mockBeanFactory(beanRegistryMock);
- return null;
- }).when(beanRegistryMock).registerBeanDefinition(eq("addPet"), any(BeanDefinition.class));
+ fixture = new HttpScenarioGenerator(new Resources.ClasspathResource(
+ "swagger/petstore-"+version+".json"));
+
+ String context = "/petstore/"+ version ;
doAnswer(invocation -> {
BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
-
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(0, String.class).getValue(), "/v2/pet/{petId}");
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(1, RequestMethod.class).getValue(), RequestMethod.GET);
- assertNotNull(scenario.getConstructorArgumentValues().getArgumentValue(2, Operation.class).getValue());
- assertNull(scenario.getPropertyValues().get("inboundDataDictionary"));
- assertNull(scenario.getPropertyValues().get("outboundDataDictionary"));
-
+ assertBeanDefinition(scenario, context+"/pet", "POST_/petstore/"+version+"/pet", "post", false);
return null;
- }).when(beanRegistryMock).registerBeanDefinition(eq("getPetById"), any(BeanDefinition.class));
+ }).when(beanRegistryMock).registerBeanDefinition(eq("POST_/petstore/"+version+"/pet"), any(BeanDefinition.class));
doAnswer(invocation -> {
BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
+ assertBeanDefinition(scenario, context+"/pet/{petId}", "GET_/petstore/"+version+"/pet/{petId}", "get", false);
+ return null;
+ }).when(beanRegistryMock).registerBeanDefinition(eq("GET_/petstore/"+version+"/pet/{petId}"), any(BeanDefinition.class));
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(0, String.class).getValue(), "/v2/pet/{petId}");
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(1, RequestMethod.class).getValue(), RequestMethod.DELETE);
- assertNotNull(scenario.getConstructorArgumentValues().getArgumentValue(2, Operation.class).getValue());
- assertNull(scenario.getPropertyValues().get("inboundDataDictionary"));
- assertNull(scenario.getPropertyValues().get("outboundDataDictionary"));
-
+ doAnswer(invocation -> {
+ BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
+ assertBeanDefinition(scenario, context+"/pet/{petId}", "DELETE_/petstore/"+version+"/pet/{petId}", "delete", false);
return null;
- }).when(beanRegistryMock).registerBeanDefinition(eq("deletePet"), any(BeanDefinition.class));
+ }).when(beanRegistryMock).registerBeanDefinition(eq("DELETE_/petstore/"+version+"/pet/{petId}"), any(BeanDefinition.class));
fixture.postProcessBeanFactory(beanRegistryMock);
- verify(beanRegistryMock).registerBeanDefinition(eq("addPet"), any(BeanDefinition.class));
- verify(beanRegistryMock).registerBeanDefinition(eq("getPetById"), any(BeanDefinition.class));
- verify(beanRegistryMock).registerBeanDefinition(eq("deletePet"), any(BeanDefinition.class));
+ verify(beanRegistryMock).registerBeanDefinition(eq("POST_/petstore/"+version+"/pet"), any(BeanDefinition.class));
+ verify(beanRegistryMock).registerBeanDefinition(eq("GET_/petstore/"+version+"/pet/{petId}"), any(BeanDefinition.class));
+ verify(beanRegistryMock).registerBeanDefinition(eq("DELETE_/petstore/"+version+"/pet/{petId}"), any(BeanDefinition.class));
}
- @Test
- void testGenerateScenariosWithDataDictionaries() {
+ @ParameterizedTest
+ @ValueSource(strings={"v2", "v3"})
+ void testGenerateScenariosWithDataDictionariesAtRootContext(String version) {
+ DefaultListableBeanFactory beanRegistryMock = mock();
+ mockBeanFactory(beanRegistryMock);
+
+ fixture = new HttpScenarioGenerator(new Resources.ClasspathResource(
+ "swagger/petstore-"+version+".json"));
+ fixture.setContextPath("/services/rest2");
+
+ String addPetScenarioId = "POST_/petstore/"+version+"/pet";
+ String getPetScenarioId = "GET_/petstore/"+version+"/pet/{petId}";
+ String deletePetScenarioId = "DELETE_/petstore/"+version+"/pet/{petId}";
+
+ String context = fixture.getContextPath()+"/petstore/"+ version ;
+
doReturn(true).when(beanRegistryMock).containsBeanDefinition("inboundJsonDataDictionary");
doReturn(true).when(beanRegistryMock).containsBeanDefinition("outboundJsonDataDictionary");
doAnswer(invocation -> {
BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
-
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(0, String.class).getValue(), "/v2/pet");
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(1, RequestMethod.class).getValue(), RequestMethod.POST);
- assertNotNull(scenario.getConstructorArgumentValues().getArgumentValue(2, Operation.class).getValue());
- assertNotNull(scenario.getPropertyValues().get("inboundDataDictionary"));
- assertNotNull(scenario.getPropertyValues().get("outboundDataDictionary"));
-
+ assertBeanDefinition(scenario, context+"/pet", addPetScenarioId, "post", true);
return null;
- }).when(beanRegistryMock).registerBeanDefinition(eq("addPet"), any(BeanDefinition.class));
+ }).when(beanRegistryMock).registerBeanDefinition(eq(addPetScenarioId), any(BeanDefinition.class));
doAnswer(invocation -> {
BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
-
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(0, String.class).getValue(), "/v2/pet/{petId}");
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(1, RequestMethod.class).getValue(), RequestMethod.GET);
- assertNotNull(scenario.getConstructorArgumentValues().getArgumentValue(2, Operation.class).getValue());
- assertNotNull(scenario.getPropertyValues().get("inboundDataDictionary"));
- assertNotNull(scenario.getPropertyValues().get("outboundDataDictionary"));
-
+ assertBeanDefinition(scenario, context+"/pet/{petId}", getPetScenarioId, "get", true);
return null;
- }).when(beanRegistryMock).registerBeanDefinition(eq("getPetById"), any(BeanDefinition.class));
+ }).when(beanRegistryMock).registerBeanDefinition(eq(getPetScenarioId), any(BeanDefinition.class));
doAnswer(invocation -> {
BeanDefinition scenario = (BeanDefinition) invocation.getArguments()[1];
-
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(0, String.class).getValue(), "/v2/pet/{petId}");
- assertEquals(scenario.getConstructorArgumentValues().getArgumentValue(1, RequestMethod.class).getValue(), RequestMethod.DELETE);
- assertNotNull(scenario.getConstructorArgumentValues().getArgumentValue(2, Operation.class).getValue());
- assertNotNull(scenario.getPropertyValues().get("inboundDataDictionary"));
- assertNotNull(scenario.getPropertyValues().get("outboundDataDictionary"));
-
+ assertBeanDefinition(scenario, context+"/pet/{petId}",deletePetScenarioId,"delete", true);
return null;
- }).when(beanRegistryMock).registerBeanDefinition(eq("deletePet"), any(BeanDefinition.class));
+ }).when(beanRegistryMock).registerBeanDefinition(eq(deletePetScenarioId), any(BeanDefinition.class));
fixture.postProcessBeanFactory(beanRegistryMock);
- verify(beanRegistryMock).registerBeanDefinition(eq("addPet"), any(BeanDefinition.class));
- verify(beanRegistryMock).registerBeanDefinition(eq("getPetById"), any(BeanDefinition.class));
- verify(beanRegistryMock).registerBeanDefinition(eq("deletePet"), any(BeanDefinition.class));
+ verify(beanRegistryMock).registerBeanDefinition(eq(addPetScenarioId), any(BeanDefinition.class));
+ verify(beanRegistryMock).registerBeanDefinition(eq(getPetScenarioId), any(BeanDefinition.class));
+ verify(beanRegistryMock).registerBeanDefinition(eq(deletePetScenarioId), any(BeanDefinition.class));
+ }
+
+ private void mockBeanFactory(BeanFactory beanFactory) {
+ doReturn(new SimulatorRestConfigurationProperties()).when(beanFactory).getBean(SimulatorRestConfigurationProperties.class);
+ doThrow(new BeansException("No such bean") {
+ }).when(beanFactory).getBean(HttpResponseActionBuilderProvider.class);
+ }
+
+ private void assertBeanDefinition(BeanDefinition scenario, String path, String scenarioId, String method, boolean withDictionaries) {
+ assertThat(getConstructorArgument(scenario, 0)).isEqualTo( path);
+ assertThat(getConstructorArgument(scenario, 1)).isEqualTo( scenarioId);
+ assertThat(getConstructorArgument(scenario, 2)).isInstanceOf(OpenApiSpecification.class);
+ assertThat(getConstructorArgument(scenario, 3)).isInstanceOf(OasOperation.class);
+ assertThat(((OasOperation)getConstructorArgument(scenario, 3)).getMethod()).isEqualTo(method);
+
+ if (withDictionaries) {
+ assertThat(scenario.getPropertyValues().get("inboundDataDictionary")).isNotNull();
+ assertThat(scenario.getPropertyValues().get("outboundDataDictionary")).isNotNull();
+ } else {
+ assertThat(scenario.getPropertyValues().get("inboundDataDictionary")).isNull();
+ assertThat(scenario.getPropertyValues().get("outboundDataDictionary")).isNull();
+ }
+ }
+
+ private static Object getConstructorArgument(BeanDefinition scenario, int index) {
+ ValueHolder argumentValue = scenario.getConstructorArgumentValues()
+ .getArgumentValue(index, String.class);
+ assertThat(argumentValue).isNotNull();
+ return argumentValue.getValue();
+ }
+
+ private void assertScenarioProperties(HttpOperationScenario scenario, String path, String operationId, String method) {
+ assertThat(scenario).extracting(HttpOperationScenario::getPath, HttpOperationScenario::getScenarioId).containsExactly(path, operationId);
+ assertThat(scenario.getMethod()).isEqualTo(method);
+ assertThat(scenario.getOperation()).isNotNull().extracting(OasOperation::getMethod).isEqualTo(method.toLowerCase());
}
}
diff --git a/simulator-spring-boot/src/test/resources/data/addPet.json b/simulator-spring-boot/src/test/resources/data/addPet.json
new file mode 100644
index 000000000..fae210be0
--- /dev/null
+++ b/simulator-spring-boot/src/test/resources/data/addPet.json
@@ -0,0 +1,15 @@
+{
+ "id": 0,
+ "category": {
+ "id": 0,
+ "name": "string"
+ },
+ "name": "doggie",
+ "photoUrls": [
+ "string"
+ ],
+ "tags": [
+ {}
+ ],
+ "status": "available"
+}
diff --git a/simulator-spring-boot/src/test/resources/data/addPet_incorrect.json b/simulator-spring-boot/src/test/resources/data/addPet_incorrect.json
new file mode 100644
index 000000000..0e4b8f388
--- /dev/null
+++ b/simulator-spring-boot/src/test/resources/data/addPet_incorrect.json
@@ -0,0 +1,15 @@
+{
+ "wrong_id_property": 0,
+ "category": {
+ "id": 0,
+ "name": "string"
+ },
+ "name": "doggie",
+ "photoUrls": [
+ "string"
+ ],
+ "tags": [
+ {}
+ ],
+ "status": "available"
+}
diff --git a/simulator-spring-boot/src/test/resources/swagger/swagger-api.json b/simulator-spring-boot/src/test/resources/swagger/petstore-v2.json
similarity index 99%
rename from simulator-spring-boot/src/test/resources/swagger/swagger-api.json
rename to simulator-spring-boot/src/test/resources/swagger/petstore-v2.json
index bfd4ee2fb..f1012b5c4 100644
--- a/simulator-spring-boot/src/test/resources/swagger/swagger-api.json
+++ b/simulator-spring-boot/src/test/resources/swagger/petstore-v2.json
@@ -1,5 +1,5 @@
{
- "swagger": "3.0.3",
+ "swagger": "2.0",
"info": {
"version": "1.0.0",
"title": "Swagger Petstore",
@@ -9,7 +9,7 @@
}
},
"host": "localhost",
- "basePath": "/v2",
+ "basePath": "/petstore/v2",
"schemes": [
"http"
],
@@ -281,4 +281,4 @@
}
}
}
-}
\ No newline at end of file
+}
diff --git a/simulator-spring-boot/src/test/resources/swagger/petstore-v3.json b/simulator-spring-boot/src/test/resources/swagger/petstore-v3.json
new file mode 100644
index 000000000..6c2b5bdfb
--- /dev/null
+++ b/simulator-spring-boot/src/test/resources/swagger/petstore-v3.json
@@ -0,0 +1,254 @@
+{
+ "openapi": "3.0.2",
+ "info": {
+ "title": "Swagger Petstore",
+ "version": "1.0.1",
+ "description": "This is a sample server Petstore server.",
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ }
+ },
+ "servers": [
+ {
+ "url": "http://localhost/petstore/v3"
+ }
+ ],
+ "paths": {
+ "/pet": {
+ "post": {
+ "requestBody": {
+ "description": "Pet object that needs to be added to the store",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "required": true
+ },
+ "tags": [
+ "pet"
+ ],
+ "responses": {
+ "201": {
+ "description": "Created"
+ },
+ "405": {
+ "description": "Invalid input"
+ }
+ },
+ "operationId": "addPet",
+ "summary": "Add a new pet to the store",
+ "description": ""
+ }
+ },
+ "/pet/{petId}": {
+ "get": {
+ "tags": [
+ "pet"
+ ],
+ "parameters": [
+ {
+ "name": "petId",
+ "description": "ID of pet to return",
+ "schema": {
+ "format": "int64",
+ "type": "integer"
+ },
+ "in": "path",
+ "required": true
+ },
+ {
+ "name": "verbose",
+ "description": "Output details",
+ "schema": {
+ "type": "boolean"
+ },
+ "in": "query",
+ "required": false
+ }
+ ],
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "description": "successful operation"
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ }
+ },
+ "operationId": "getPetById",
+ "summary": "Find pet by ID",
+ "description": "Returns a single pet"
+ },
+ "delete": {
+ "tags": [
+ "pet"
+ ],
+ "parameters": [
+ {
+ "name": "api_key",
+ "schema": {
+ "type": "string"
+ },
+ "in": "header",
+ "required": false
+ },
+ {
+ "name": "petId",
+ "description": "Pet id to delete",
+ "schema": {
+ "format": "int64",
+ "type": "integer"
+ },
+ "in": "path",
+ "required": true
+ }
+ ],
+ "responses": {
+ "204": {
+ "description": "No content"
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ }
+ },
+ "operationId": "deletePet",
+ "summary": "Deletes a pet",
+ "description": ""
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Category": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "format": "int64",
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "Category"
+ }
+ },
+ "Tag": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "format": "int64",
+ "type": "integer"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "Tag"
+ }
+ },
+ "Pet": {
+ "required": [
+ "category",
+ "name",
+ "status"
+ ],
+ "type": "object",
+ "properties": {
+ "id": {
+ "format": "int64",
+ "type": "integer"
+ },
+ "category": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "name": {
+ "type": "string",
+ "example": "doggie"
+ },
+ "photoUrls": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "xml": {
+ "name": "photoUrl",
+ "wrapped": true
+ }
+ },
+ "tags": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Tag"
+ },
+ "xml": {
+ "name": "tag",
+ "wrapped": true
+ }
+ },
+ "status": {
+ "description": "pet status in the store",
+ "enum": [
+ "available",
+ "pending",
+ "sold"
+ ],
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "Pet"
+ }
+ },
+ "ApiResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "format": "int32",
+ "type": "integer"
+ },
+ "type": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ },
+ "tags": [
+ {
+ "name": "pet",
+ "description": "Everything about your Pets"
+ }
+ ]
+}