diff --git a/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/Simulator.java b/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/Simulator.java
index b06ad5291..12f10849b 100644
--- a/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/Simulator.java
+++ b/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/Simulator.java
@@ -21,13 +21,15 @@
import org.citrusframework.endpoint.adapter.StaticEndpointAdapter;
import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.message.Message;
-import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
-import org.citrusframework.simulator.scenario.mapper.ScenarioMappers;
+import org.citrusframework.openapi.OpenApiRepository;
import org.citrusframework.simulator.http.HttpRequestAnnotationScenarioMapper;
import org.citrusframework.simulator.http.HttpRequestPathScenarioMapper;
+import org.citrusframework.simulator.http.HttpResponseActionBuilderProvider;
import org.citrusframework.simulator.http.HttpScenarioGenerator;
import org.citrusframework.simulator.http.SimulatorRestAdapter;
import org.citrusframework.simulator.http.SimulatorRestConfigurationProperties;
+import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
+import org.citrusframework.simulator.scenario.mapper.ScenarioMappers;
import org.citrusframework.spi.Resources;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@@ -47,12 +49,13 @@ public static void main(String[] args) {
@Override
public ScenarioMapper scenarioMapper() {
return ScenarioMappers.of(new HttpRequestPathScenarioMapper(),
- new HttpRequestAnnotationScenarioMapper());
+ new HttpRequestAnnotationScenarioMapper());
}
@Override
- public List urlMappings(SimulatorRestConfigurationProperties simulatorRestConfiguration) {
- return List.of("/petstore/v2/**");
+ public List urlMappings(
+ SimulatorRestConfigurationProperties simulatorRestConfiguration) {
+ return List.of("/petstore/v2/**", "/petstore/api/v3/**", "/pingapi/v1/**");
}
@Override
@@ -67,8 +70,31 @@ protected Message handleMessageInternal(Message message) {
@Bean
public static HttpScenarioGenerator scenarioGenerator() {
- HttpScenarioGenerator generator = new HttpScenarioGenerator(new Resources.ClasspathResource("swagger/petstore-api.json"));
+ HttpScenarioGenerator generator = new HttpScenarioGenerator(
+ new Resources.ClasspathResource("swagger/petstore-api.json"));
generator.setContextPath("/petstore");
return generator;
}
+
+ @Bean
+ public static OpenApiRepository petstoreRepository() {
+ OpenApiRepository openApiRepository = new OpenApiRepository();
+ openApiRepository.setRootContextPath("/petstore");
+ openApiRepository.setLocations(List.of("openapi/*.json"));
+ return openApiRepository;
+ }
+
+ @Bean
+ public static OpenApiRepository pingRepository() {
+ OpenApiRepository openApiRepository = new OpenApiRepository();
+ openApiRepository.setLocations(List.of("openapi/*.yaml"));
+ return openApiRepository;
+ }
+
+ @Bean
+ static HttpResponseActionBuilderProvider httpResponseActionBuilderProvider() {
+ return new SpecificPingResponseMessageBuilder();
+ }
+
+
}
diff --git a/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/SpecificPingResponseMessageBuilder.java b/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/SpecificPingResponseMessageBuilder.java
new file mode 100644
index 000000000..5f4d114c3
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/main/java/org/citrusframework/simulator/sample/SpecificPingResponseMessageBuilder.java
@@ -0,0 +1,140 @@
+package org.citrusframework.simulator.sample;
+
+import static java.lang.String.format;
+
+import io.apicurio.datamodels.openapi.models.OasOperation;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import org.apache.commons.lang3.function.TriFunction;
+import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
+import org.citrusframework.http.message.HttpMessage;
+import org.citrusframework.http.message.HttpMessageHeaders;
+import org.citrusframework.message.MessageType;
+import org.citrusframework.openapi.actions.OpenApiActionBuilder;
+import org.citrusframework.openapi.actions.OpenApiServerActionBuilder;
+import org.citrusframework.openapi.actions.OpenApiServerResponseActionBuilder;
+import org.citrusframework.simulator.http.HttpOperationScenario;
+import org.citrusframework.simulator.http.HttpResponseActionBuilderProvider;
+import org.citrusframework.simulator.scenario.ScenarioRunner;
+import org.citrusframework.simulator.scenario.SimulatorScenario;
+import org.springframework.http.MediaType;
+
+/**
+ * {@link HttpResponseActionBuilderProvider} that provides specific responses for dedicated ping
+ * calls. Shows, how to use a {@link HttpResponseActionBuilderProvider} to control the random
+ * message generation.
+ */
+public class SpecificPingResponseMessageBuilder implements HttpResponseActionBuilderProvider {
+
+ private static final int MISSING_ID = Integer.MIN_VALUE;
+
+ /**
+ * Function that returns null to indicate, that the provider does not provide a builder for the given scenario.
+ */
+ private static final TriFunction NULL_RESPONSE = SpecificPingResponseMessageBuilder::createNull;
+
+ /**
+ * Map to store specific functions per ping id.
+ */
+ private static final Map> SPECIFC_BUILDER_MAP = new HashMap<>();
+
+ // Specific responses for some ids, all others will be handled by returning null and letting the random generator do its work.
+ static {
+ SPECIFC_BUILDER_MAP.put(15000,
+ SpecificPingResponseMessageBuilder::createResponseWithDedicatedRequiredHeader);
+ SPECIFC_BUILDER_MAP.put(10000,
+ SpecificPingResponseMessageBuilder::createResponseWithMessageAndHeaders);
+ SPECIFC_BUILDER_MAP.put(5000, SpecificPingResponseMessageBuilder::createResponseWithSpecificBody);
+ SPECIFC_BUILDER_MAP.put(4000,
+ SpecificPingResponseMessageBuilder::createResponseWithRandomGenerationSuppressed);
+ }
+
+ @Override
+ public HttpServerResponseActionBuilder provideHttpServerResponseActionBuilder(
+ ScenarioRunner scenarioRunner, SimulatorScenario simulatorScenario,
+ HttpMessage receivedMessage) {
+
+ if (!(simulatorScenario instanceof HttpOperationScenario httpOperationScenario)) {
+ return null;
+ }
+
+ OpenApiServerActionBuilder openApiServerActionBuilder = new OpenApiActionBuilder(
+ httpOperationScenario.getOpenApiSpecification()).server(scenarioRunner.getScenarioEndpoint());
+
+ return SPECIFC_BUILDER_MAP.getOrDefault(getIdFromPingRequest(receivedMessage), NULL_RESPONSE).apply(openApiServerActionBuilder, httpOperationScenario.getOperation(), receivedMessage);
+ }
+
+ private static Integer getIdFromPingRequest(HttpMessage httpMessage) {
+ String uri = httpMessage.getUri();
+ Pattern pattern = Pattern.compile("/pingapi/v1/ping/(\\d*)");
+ Matcher matcher = pattern.matcher(uri);
+ if (matcher.matches()) {
+ return Integer.parseInt(matcher.group(1));
+ }
+ return MISSING_ID;
+ }
+
+ /**
+ * Sample to prove, that random data generation can be suppressed. Note that the generated
+ * response is thus invalid and will result in an error.
+ */
+ private static OpenApiServerResponseActionBuilder createResponseWithRandomGenerationSuppressed(
+ OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
+ HttpMessage receivedMessage) {
+ OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
+ oasOperation.operationId, "200").enableRandomGeneration(false);
+ sendMessageBuilder.message().body(format("{\"id\": %d, \"pingTime\": %d}",
+ getIdFromPingRequest(receivedMessage), System.currentTimeMillis()));
+ return sendMessageBuilder;
+ }
+
+ /**
+ * Sample to prove, that the body content can be controlled, while headers will be generated by
+ * random generator.
+ */
+ private static OpenApiServerResponseActionBuilder createResponseWithSpecificBody(
+ OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
+ HttpMessage receivedMessage) {
+ OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
+ oasOperation.operationId, "200");
+ sendMessageBuilder.message().body(format("{\"id\": %d, \"pingCount\": %d}",
+ getIdFromPingRequest(receivedMessage), System.currentTimeMillis()));
+ return sendMessageBuilder;
+ }
+
+ /**
+ * Sample to prove, that the status, response and headers can be controlled and are not
+ * overwritten by random generator.
+ */
+ private static OpenApiServerResponseActionBuilder createResponseWithMessageAndHeaders(
+ OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
+ HttpMessage receivedMessage) {
+ OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
+ oasOperation.operationId, "400", receivedMessage.getAccept());
+ sendMessageBuilder.message().type(MessageType.PLAINTEXT)
+ .header(HttpMessageHeaders.HTTP_CONTENT_TYPE, MediaType.TEXT_PLAIN_VALUE)
+ .header("Ping-Time", "1").body("Requests with id == 10000 cannot be processed!");
+ return sendMessageBuilder;
+ }
+
+ /**
+ * Sample to prove, that a preset header can be controlled, while generating a valid random
+ * response.
+ */
+ private static OpenApiServerResponseActionBuilder createResponseWithDedicatedRequiredHeader(
+ OpenApiServerActionBuilder openApiServerActionBuilder, OasOperation oasOperation,
+ HttpMessage receivedMessage) {
+ OpenApiServerResponseActionBuilder sendMessageBuilder = openApiServerActionBuilder.send(
+ oasOperation.operationId, "200", receivedMessage.getAccept());
+ sendMessageBuilder.message().header("Ping-Time", "0");
+ return sendMessageBuilder;
+ }
+
+ private static OpenApiServerResponseActionBuilder createNull(
+ OpenApiServerActionBuilder ignoreOpenApiServerActionBuilder,
+ OasOperation ignoreOasOperation, HttpMessage ignoreReceivedMessage) {
+ return null;
+ }
+}
diff --git a/simulator-samples/sample-swagger/src/main/resources/openapi/README.md b/simulator-samples/sample-swagger/src/main/resources/openapi/README.md
new file mode 100644
index 000000000..12756841b
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/main/resources/openapi/README.md
@@ -0,0 +1,2 @@
+Note, that the petstore-v3.json has been slightly modified from its original version.
+OK messages have been added, where missing, to be able to activate the response validation feature.
diff --git a/simulator-samples/sample-swagger/src/main/resources/openapi/petstore-v3.json b/simulator-samples/sample-swagger/src/main/resources/openapi/petstore-v3.json
new file mode 100644
index 000000000..4e0e16add
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/main/resources/openapi/petstore-v3.json
@@ -0,0 +1,1228 @@
+{
+ "openapi": "3.0.2",
+ "info": {
+ "title": "Swagger Petstore - OpenAPI 3.0",
+ "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)",
+ "termsOfService": "http://swagger.io/terms/",
+ "contact": {
+ "email": "apiteam@swagger.io"
+ },
+ "license": {
+ "name": "Apache 2.0",
+ "url": "http://www.apache.org/licenses/LICENSE-2.0.html"
+ },
+ "version": "1.0.19"
+ },
+ "externalDocs": {
+ "description": "Find out more about Swagger",
+ "url": "http://swagger.io"
+ },
+ "servers": [
+ {
+ "url": "/api/v3"
+ }
+ ],
+ "tags": [
+ {
+ "name": "pet",
+ "description": "Everything about your Pets",
+ "externalDocs": {
+ "description": "Find out more",
+ "url": "http://swagger.io"
+ }
+ },
+ {
+ "name": "store",
+ "description": "Access to Petstore orders",
+ "externalDocs": {
+ "description": "Find out more about our store",
+ "url": "http://swagger.io"
+ }
+ },
+ {
+ "name": "user",
+ "description": "Operations about user"
+ }
+ ],
+ "paths": {
+ "/pet": {
+ "put": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Update an existing pet",
+ "description": "Update an existing pet by Id",
+ "operationId": "updatePet",
+ "requestBody": {
+ "description": "Update an existent pet in the store",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ },
+ "405": {
+ "description": "Validation exception"
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ },
+ "post": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Add a new pet to the store",
+ "description": "Add a new pet to the store",
+ "operationId": "addPet",
+ "requestBody": {
+ "description": "Create a new pet in the store",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "required": true
+ },
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Invalid input"
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ }
+ },
+ "/pet/findByStatus": {
+ "get": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Finds Pets by status",
+ "description": "Multiple status values can be provided with comma separated strings",
+ "operationId": "findPetsByStatus",
+ "parameters": [
+ {
+ "name": "status",
+ "in": "query",
+ "description": "Status values that need to be considered for filter",
+ "required": false,
+ "explode": true,
+ "schema": {
+ "type": "string",
+ "default": "available",
+ "enum": [
+ "available",
+ "pending",
+ "sold"
+ ]
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid status value"
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ }
+ },
+ "/pet/findByTags": {
+ "get": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Finds Pets by tags",
+ "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.",
+ "operationId": "findPetsByTags",
+ "parameters": [
+ {
+ "name": "tags",
+ "in": "query",
+ "description": "Tags to filter by",
+ "required": false,
+ "explode": true,
+ "schema": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid tag value"
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ }
+ },
+ "/pet/{petId}": {
+ "get": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Find pet by ID",
+ "description": "Returns a single pet",
+ "operationId": "getPetById",
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "ID of pet to return",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Pet"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Pet not found"
+ }
+ },
+ "security": [
+ {
+ "api_key": []
+ },
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ },
+ "post": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Updates a pet in the store with form data",
+ "description": "",
+ "operationId": "updatePetWithForm",
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "ID of pet that needs to be updated",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ },
+ {
+ "name": "name",
+ "in": "query",
+ "description": "Name of pet that needs to be updated",
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "status",
+ "in": "query",
+ "description": "Status of pet that needs to be updated",
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "405": {
+ "description": "Invalid input"
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ },
+ "delete": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "Deletes a pet",
+ "description": "",
+ "operationId": "deletePet",
+ "parameters": [
+ {
+ "name": "api_key",
+ "in": "header",
+ "description": "",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "Pet id to delete",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "Successful operation"
+ },
+ "400": {
+ "description": "Invalid pet value"
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ }
+ },
+ "/pet/{petId}/uploadImage": {
+ "post": {
+ "tags": [
+ "pet"
+ ],
+ "summary": "uploads an image",
+ "description": "",
+ "operationId": "uploadFile",
+ "parameters": [
+ {
+ "name": "petId",
+ "in": "path",
+ "description": "ID of pet to update",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ },
+ {
+ "name": "additionalMetadata",
+ "in": "query",
+ "description": "Additional Metadata",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "content": {
+ "application/octet-stream": {
+ "schema": {
+ "type": "string",
+ "format": "binary"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ApiResponse"
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "petstore_auth": [
+ "write:pets",
+ "read:pets"
+ ]
+ }
+ ]
+ }
+ },
+ "/store/inventory": {
+ "get": {
+ "tags": [
+ "store"
+ ],
+ "summary": "Returns pet inventories by status",
+ "description": "Returns a map of status codes to quantities",
+ "operationId": "getInventory",
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "additionalProperties": {
+ "type": "integer",
+ "format": "int32"
+ }
+ }
+ }
+ }
+ }
+ },
+ "security": [
+ {
+ "api_key": []
+ }
+ ]
+ }
+ },
+ "/store/order": {
+ "post": {
+ "tags": [
+ "store"
+ ],
+ "summary": "Place an order for a pet",
+ "description": "Place a new order in the store",
+ "operationId": "placeOrder",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ },
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "405": {
+ "description": "Invalid input"
+ }
+ }
+ }
+ },
+ "/store/order/{orderId}": {
+ "get": {
+ "tags": [
+ "store"
+ ],
+ "summary": "Find purchase order by ID",
+ "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.",
+ "operationId": "getOrderById",
+ "parameters": [
+ {
+ "name": "orderId",
+ "in": "path",
+ "description": "ID of order that needs to be fetched",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Order"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Order not found"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "store"
+ ],
+ "summary": "Delete purchase order by ID",
+ "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors",
+ "operationId": "deleteOrder",
+ "parameters": [
+ {
+ "name": "orderId",
+ "in": "path",
+ "description": "ID of the order that needs to be deleted",
+ "required": true,
+ "schema": {
+ "type": "integer",
+ "format": "int64"
+ }
+ }
+ ],
+ "responses": {
+ "400": {
+ "description": "Invalid ID supplied"
+ },
+ "404": {
+ "description": "Order not found"
+ }
+ }
+ }
+ },
+ "/user": {
+ "post": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Create user",
+ "description": "This can only be done by the logged in user.",
+ "operationId": "createUser",
+ "requestBody": {
+ "description": "Created user object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "responses": {
+ "default": {
+ "description": "successful operation",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ }
+ }
+ },
+ "/user/createWithList": {
+ "post": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Creates list of users with given input array",
+ "description": "Creates list of users with given input array",
+ "operationId": "createUsersWithListInput",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ },
+ "responses": {
+ "200": {
+ "description": "Successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "default": {
+ "description": "successful operation"
+ }
+ }
+ }
+ },
+ "/user/login": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Logs user into the system",
+ "description": "",
+ "operationId": "loginUser",
+ "parameters": [
+ {
+ "name": "username",
+ "in": "query",
+ "description": "The user name for login",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "password",
+ "in": "query",
+ "description": "The password for login in clear text",
+ "required": false,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "headers": {
+ "X-Rate-Limit": {
+ "description": "calls per hour allowed by the user",
+ "schema": {
+ "type": "integer",
+ "format": "int32"
+ }
+ },
+ "X-Expires-After": {
+ "description": "date in UTC when token expires",
+ "schema": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "content": {
+ "application/xml": {
+ "schema": {
+ "type": "string"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "type": "string"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid username/password supplied"
+ }
+ }
+ }
+ },
+ "/user/logout": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Logs out current logged in user session",
+ "description": "",
+ "operationId": "logoutUser",
+ "parameters": [],
+ "responses": {
+ "default": {
+ "description": "successful operation"
+ }
+ }
+ }
+ },
+ "/user/{username}": {
+ "get": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Get user by user name",
+ "description": "",
+ "operationId": "getUserByName",
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "description": "The name that needs to be fetched. Use user1 for testing. ",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "200": {
+ "description": "successful operation",
+ "content": {
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid username supplied"
+ },
+ "404": {
+ "description": "User not found"
+ }
+ }
+ },
+ "put": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Update user",
+ "description": "This can only be done by the logged in user.",
+ "operationId": "updateUser",
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "description": "name that needs to be updated",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "requestBody": {
+ "description": "Update an existent user in the store",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/xml": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ },
+ "application/x-www-form-urlencoded": {
+ "schema": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ },
+ "responses": {
+ "default": {
+ "description": "successful operation"
+ }
+ }
+ },
+ "delete": {
+ "tags": [
+ "user"
+ ],
+ "summary": "Delete user",
+ "description": "This can only be done by the logged in user.",
+ "operationId": "deleteUser",
+ "parameters": [
+ {
+ "name": "username",
+ "in": "path",
+ "description": "The name that needs to be deleted",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "responses": {
+ "400": {
+ "description": "Invalid username supplied"
+ },
+ "404": {
+ "description": "User not found"
+ }
+ }
+ }
+ }
+ },
+ "components": {
+ "schemas": {
+ "Order": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "example": 10
+ },
+ "petId": {
+ "type": "integer",
+ "format": "int64",
+ "example": 198772
+ },
+ "quantity": {
+ "type": "integer",
+ "format": "int32",
+ "example": 7
+ },
+ "shipDate": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "status": {
+ "type": "string",
+ "description": "Order Status",
+ "example": "approved",
+ "enum": [
+ "placed",
+ "approved",
+ "delivered"
+ ]
+ },
+ "complete": {
+ "type": "boolean"
+ }
+ },
+ "xml": {
+ "name": "order"
+ }
+ },
+ "Customer": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "example": 100000
+ },
+ "username": {
+ "type": "string",
+ "example": "fehguy"
+ },
+ "address": {
+ "type": "array",
+ "xml": {
+ "name": "addresses",
+ "wrapped": true
+ },
+ "items": {
+ "$ref": "#/components/schemas/Address"
+ }
+ }
+ },
+ "xml": {
+ "name": "customer"
+ }
+ },
+ "Address": {
+ "type": "object",
+ "properties": {
+ "street": {
+ "type": "string",
+ "example": "437 Lytton"
+ },
+ "city": {
+ "type": "string",
+ "example": "Palo Alto"
+ },
+ "state": {
+ "type": "string",
+ "example": "CA"
+ },
+ "zip": {
+ "type": "string",
+ "example": "94301"
+ }
+ },
+ "xml": {
+ "name": "address"
+ }
+ },
+ "Category": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "example": 1
+ },
+ "name": {
+ "type": "string",
+ "example": "Dogs"
+ }
+ },
+ "xml": {
+ "name": "category"
+ }
+ },
+ "User": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "example": 10
+ },
+ "username": {
+ "type": "string",
+ "example": "theUser"
+ },
+ "firstName": {
+ "type": "string",
+ "example": "John"
+ },
+ "lastName": {
+ "type": "string",
+ "example": "James"
+ },
+ "email": {
+ "type": "string",
+ "example": "john@email.com"
+ },
+ "password": {
+ "type": "string",
+ "example": "12345"
+ },
+ "phone": {
+ "type": "string",
+ "example": "12345"
+ },
+ "userStatus": {
+ "type": "integer",
+ "description": "User Status",
+ "format": "int32",
+ "example": 1
+ }
+ },
+ "xml": {
+ "name": "user"
+ }
+ },
+ "Tag": {
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64"
+ },
+ "name": {
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "tag"
+ }
+ },
+ "Pet": {
+ "required": [
+ "name",
+ "photoUrls"
+ ],
+ "type": "object",
+ "properties": {
+ "id": {
+ "type": "integer",
+ "format": "int64",
+ "example": 10
+ },
+ "name": {
+ "type": "string",
+ "example": "doggie"
+ },
+ "category": {
+ "$ref": "#/components/schemas/Category"
+ },
+ "photoUrls": {
+ "type": "array",
+ "xml": {
+ "wrapped": true
+ },
+ "items": {
+ "type": "string",
+ "xml": {
+ "name": "photoUrl"
+ }
+ }
+ },
+ "tags": {
+ "type": "array",
+ "xml": {
+ "wrapped": true
+ },
+ "items": {
+ "$ref": "#/components/schemas/Tag"
+ }
+ },
+ "status": {
+ "type": "string",
+ "description": "pet status in the store",
+ "enum": [
+ "available",
+ "pending",
+ "sold"
+ ]
+ }
+ },
+ "xml": {
+ "name": "pet"
+ }
+ },
+ "ApiResponse": {
+ "type": "object",
+ "properties": {
+ "code": {
+ "type": "integer",
+ "format": "int32"
+ },
+ "type": {
+ "type": "string"
+ },
+ "message": {
+ "type": "string"
+ }
+ },
+ "xml": {
+ "name": "##default"
+ }
+ }
+ },
+ "requestBodies": {
+ "Pet": {
+ "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"
+ }
+ }
+ }
+ },
+ "UserArray": {
+ "description": "List of user object",
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/User"
+ }
+ }
+ }
+ }
+ }
+ },
+ "securitySchemes": {
+ "petstore_auth": {
+ "type": "oauth2",
+ "flows": {
+ "implicit": {
+ "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize",
+ "scopes": {
+ "write:pets": "modify pets in your account",
+ "read:pets": "read your pets"
+ }
+ }
+ }
+ },
+ "api_key": {
+ "type": "apiKey",
+ "name": "api_key",
+ "in": "header"
+ }
+ }
+ }
+}
diff --git a/simulator-samples/sample-swagger/src/main/resources/openapi/ping-v1.yaml b/simulator-samples/sample-swagger/src/main/resources/openapi/ping-v1.yaml
new file mode 100644
index 000000000..b1375a92b
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/main/resources/openapi/ping-v1.yaml
@@ -0,0 +1,112 @@
+openapi: 3.0.1
+info:
+ title: Ping API
+ description: Provides ping and pong endpoints for testing
+ version: 1.0
+ license:
+ name: Apache 2.0
+ url: http://www.apache.org/licenses/LICENSE-2.0.html
+servers:
+ - url: http://localhost:9000/pingapi/v1
+
+paths:
+ /ping/{id}:
+ put:
+ tags:
+ - ping
+ summary: Puts a ping
+ operationId: doPing
+ parameters:
+ - name: id
+ in: path
+ description: Id to ping
+ required: true
+ schema:
+ type: integer
+ format: int64
+ requestBody:
+ description: Ping data
+ required: true
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PingRequest'
+ responses:
+ 200:
+ description: Successful operation
+ headers:
+ Ping-Time:
+ description: Response time
+ required: true
+ schema:
+ type: integer
+ format: int64
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PingResponse'
+ 400:
+ $ref: '#/components/responses/Status400Response'
+ /pong/{id}:
+ get:
+ tags:
+ - pong
+ summary: Get a pong
+ operationId: doPong
+ parameters:
+ - name: id
+ in: path
+ description: The id to pong
+ required: true
+ schema:
+ type: integer
+ format: int64
+ responses:
+ 200:
+ description: Successful operation
+ content:
+ application/json:
+ schema:
+ $ref: '#/components/schemas/PongResponse'
+ 400:
+ $ref: '#/components/responses/Status400Response'
+components:
+ responses:
+ Status400Response:
+ description: Invalid id supplied
+ content:
+ text/plain:
+ schema:
+ type: string
+ schemas:
+ PingRequest:
+ type: object
+ properties:
+ server:
+ type: string
+ required:
+ - server
+ PingResponse:
+ type: object
+ properties:
+ id:
+ type: integer
+ format: int64
+ pingCount:
+ type: integer
+ format: int64
+ required:
+ - id
+ - pingCount
+ PongResponse:
+ type: object
+ properties:
+ id:
+ type: string
+ format: uuid
+ pongTime:
+ type: string
+ format: date-time
+ required:
+ - id
+ - pongTime
diff --git a/simulator-samples/sample-swagger/src/main/resources/swagger/README.md b/simulator-samples/sample-swagger/src/main/resources/swagger/README.md
new file mode 100644
index 000000000..bf60e9497
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/main/resources/swagger/README.md
@@ -0,0 +1,3 @@
+Note, that the petstore-api.json has been slightly modified from its original version.
+OK messages have been added, where missing, to be able to activate the response validation feature.
+Also, all simple message responses have been given a simple Message schema,
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 5fc5236b7..a66f0d7f8 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
@@ -69,8 +69,14 @@
}
],
"responses": {
+ "200": {
+ "description": "successful operation"
+ },
"405": {
- "description": "Invalid input"
+ "description": "Invalid input",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
},
"security": [
@@ -109,14 +115,30 @@
}
],
"responses": {
+ "200": {
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
+ },
"400": {
- "description": "Invalid ID supplied"
+ "description": "Invalid ID supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
+
},
"404": {
- "description": "Pet not found"
+ "description": "Pet not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"405": {
- "description": "Validation exception"
+ "description": "Validation exception",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
},
"security": [
@@ -171,7 +193,10 @@
}
},
"400": {
- "description": "Invalid status value"
+ "description": "Invalid status value",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
},
"security": [
@@ -220,7 +245,10 @@
}
},
"400": {
- "description": "Invalid tag value"
+ "description": "Invalid tag value",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
},
"security": [
@@ -264,10 +292,17 @@
}
},
"400": {
- "description": "Invalid ID supplied"
+ "description": "Invalid ID supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
+
},
"404": {
- "description": "Pet not found"
+ "description": "Pet not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
},
"security": [
@@ -316,7 +351,11 @@
],
"responses": {
"405": {
- "description": "Invalid input"
+ "description": "Invalid input",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
+
}
},
"security": [
@@ -356,11 +395,23 @@
}
],
"responses": {
+ "200": {
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
+ },
"400": {
- "description": "Invalid ID supplied"
+ "description": "Invalid ID supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"404": {
- "description": "Pet not found"
+ "description": "Pet not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
},
"security": [
@@ -491,7 +542,10 @@
}
},
"400": {
- "description": "Invalid Order"
+ "description": "Invalid Order",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -528,10 +582,16 @@
}
},
"400": {
- "description": "Invalid ID supplied"
+ "description": "Invalid ID supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"404": {
- "description": "Order not found"
+ "description": "Order not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
},
@@ -559,10 +619,16 @@
],
"responses": {
"400": {
- "description": "Invalid ID supplied"
+ "description": "Invalid ID supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"404": {
- "description": "Order not found"
+ "description": "Order not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -592,7 +658,10 @@
],
"responses": {
"default": {
- "description": "successful operation"
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -625,7 +694,10 @@
],
"responses": {
"default": {
- "description": "successful operation"
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -658,7 +730,10 @@
],
"responses": {
"default": {
- "description": "successful operation"
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -711,7 +786,10 @@
}
},
"400": {
- "description": "Invalid username/password supplied"
+ "description": "Invalid username/password supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -731,7 +809,10 @@
"parameters": [],
"responses": {
"default": {
- "description": "successful operation"
+ "description": "successful operation",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -765,10 +846,16 @@
}
},
"400": {
- "description": "Invalid username supplied"
+ "description": "Invalid username supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"404": {
- "description": "User not found"
+ "description": "User not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
},
@@ -803,10 +890,16 @@
],
"responses": {
"400": {
- "description": "Invalid user supplied"
+ "description": "Invalid user supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"404": {
- "description": "User not found"
+ "description": "User not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
},
@@ -832,10 +925,16 @@
],
"responses": {
"400": {
- "description": "Invalid username supplied"
+ "description": "Invalid username supplied",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
},
"404": {
- "description": "User not found"
+ "description": "User not found",
+ "schema": {
+ "$ref": "#/definitions/Message"
+ }
}
}
}
@@ -1026,6 +1125,15 @@
"type": "string"
}
}
+ },
+ "Message": {
+ "type": "object",
+ "required": ["message"],
+ "properties": {
+ "message": {
+ "type": "string"
+ }
+ }
}
},
"externalDocs": {
diff --git a/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/OpenApiIT.java b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/OpenApiIT.java
new file mode 100644
index 000000000..910d45f20
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/OpenApiIT.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.simulator;
+
+import static org.citrusframework.http.actions.HttpActionBuilder.http;
+
+import org.citrusframework.annotations.CitrusTest;
+import org.citrusframework.container.BeforeSuite;
+import org.citrusframework.container.SequenceBeforeSuite;
+import org.citrusframework.dsl.endpoint.CitrusEndpoints;
+import org.citrusframework.http.client.HttpClient;
+import org.citrusframework.message.MessageType;
+import org.citrusframework.simulator.sample.Simulator;
+import org.citrusframework.spi.Resources;
+import org.citrusframework.testng.spring.TestNGCitrusSpringSupport;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ContextConfiguration;
+import org.testng.annotations.Test;
+
+/**
+ * @author Christoph Deppisch
+ */
+@Test
+@ContextConfiguration(classes = OpenApiIT.EndpointConfig.class)
+public class OpenApiIT extends TestNGCitrusSpringSupport {
+
+ @Autowired
+ @Qualifier("petstoreClientV3")
+ private HttpClient petstoreClientV3;
+
+ /**
+ * Client to access simulator user interface
+ */
+ @Autowired
+ @Qualifier("simulatorUiClient")
+ private HttpClient simulatorUiClient;
+
+ @CitrusTest
+ public void uiInfoShouldSucceed() {
+ $(http().client(simulatorUiClient)
+ .send()
+ .get("/api/manage/info")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE));
+
+ $(http().client(simulatorUiClient)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body("{" +
+ "\"simulator\":" +
+ "{" +
+ "\"name\":\"REST Petstore Simulator\"," +
+ "\"version\":\"@ignore@\"" +
+ "}," +
+ "\"activeProfiles\": []" +
+ "}"));
+ }
+
+ @CitrusTest
+ public void addPetShouldSucceed() {
+ variable("name", "hasso");
+ variable("category", "dog");
+ variable("tags", "huge");
+ variable("status", "pending");
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .post("/pet")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/pet.json")));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ @CitrusTest
+ public void addPetShouldFailOnMissingName() {
+ variable("category", "dog");
+ variable("tags", "huge");
+ variable("status", "pending");
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .post("/pet")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/pet_invalid.json")));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void deletePetShouldSucceed() {
+ variable("id", "citrus:randomNumber(10)");
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .delete("/pet/${id}"));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ @CitrusTest
+ public void deletePetShouldFailOnWrongIdFormat() {
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .delete("/pet/xxxx"));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void getPetByIdShouldSucceed() {
+ variable("id", "citrus:randomNumber(10)");
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .get("/pet/${id}")
+ .message()
+ .header("api_key", "xxx_api_key")
+ .accept(MediaType.APPLICATION_JSON_VALUE));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/pet-control.json")));
+ }
+
+ @CitrusTest
+ public void getPetByIdShouldNotFailOnMissingApiKey() {
+ variable("id", "citrus:randomNumber(10)");
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .get("/pet/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE));
+
+ // api_key is not required in V3, therefore no error here
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ @CitrusTest
+ public void updatePetShouldSucceed() {
+ variable("name", "catty");
+ variable("category", "cat");
+ variable("tags", "cute");
+ variable("status", "sold");
+
+ $(http().client(petstoreClientV3)
+ .send()
+ .put("/pet")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/pet.json")));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ @CitrusTest
+ public void findByStatusShouldSucceed() {
+ $(http().client(petstoreClientV3)
+ .send()
+ .get("/pet/findByStatus")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .queryParam("status", "pending"));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body("[ citrus:readFile(templates/pet-control.json) ]"));
+ }
+
+ @CitrusTest
+ public void findByStatusShouldNotFailOnMissingQueryParameter() {
+ $(http().client(petstoreClientV3)
+ .send()
+ .get("/pet/findByStatus")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE));
+
+ // In petstore 3 status is not required.
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ @CitrusTest
+ public void findByTagsShouldSucceed() {
+ $(http().client(petstoreClientV3)
+ .send()
+ .get("/pet/findByTags")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .queryParam("tags", "huge,cute"));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body("[ citrus:readFile(templates/pet-control.json) ]"));
+ }
+
+ @CitrusTest
+ public void placeOrderShouldSucceed() {
+ $(http().client(petstoreClientV3)
+ .send()
+ .post("/store/order")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/order.json"))
+ .header("api_key", "xxx_api_key"));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ @CitrusTest
+ public void placeOrderShouldFailOnInvalidDateFormat() {
+ $(http().client(petstoreClientV3)
+ .send()
+ .post("/store/order")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/order_invalid_date.json"))
+ .header("api_key", "xxx_api_key"));
+
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void loginUserShouldSucceed() {
+ $(http().client(petstoreClientV3)
+ .send()
+ .get("/user/login")
+ .queryParam("username", "citrus:randomString(10)")
+ .queryParam("password", "citrus:randomString(8)")
+ .message()
+ .header("api_key", "xxx_api_key")
+ .accept(MediaType.APPLICATION_JSON_VALUE));
+
+ // X-Rate-Limit and X-Expires-After are not required in V3, therefore we cannot assert them here.
+ $(http().client(petstoreClientV3)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .type(MessageType.JSON)
+ .body("@notEmpty()@"));
+ }
+
+ @Configuration
+ public static class EndpointConfig {
+
+ @Bean
+ public HttpClient petstoreClientV3() {
+ return CitrusEndpoints.http().client()
+ .requestUrl(String.format("http://localhost:%s/petstore/api/v3", 8080))
+ .build();
+ }
+
+ @Bean
+ public HttpClient simulatorUiClient() {
+ return CitrusEndpoints.http().client()
+ .requestUrl(String.format("http://localhost:%s", 8080))
+ .build();
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "simulator.mode", havingValue = "embedded")
+ public BeforeSuite startEmbeddedSimulator() {
+ return new SequenceBeforeSuite.Builder().actions(
+ context -> SpringApplication.run(Simulator.class)).build();
+ }
+ }
+}
diff --git a/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/ResponseGeneratorIT.java b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/ResponseGeneratorIT.java
new file mode 100644
index 000000000..7adc0cf6e
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/ResponseGeneratorIT.java
@@ -0,0 +1,187 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.citrusframework.simulator;
+
+import static org.citrusframework.http.actions.HttpActionBuilder.http;
+import static org.citrusframework.validation.json.JsonPathMessageValidationContext.Builder.jsonPath;
+import static org.hamcrest.Matchers.equalTo;
+import static org.hamcrest.Matchers.not;
+
+import org.citrusframework.annotations.CitrusTest;
+import org.citrusframework.container.BeforeSuite;
+import org.citrusframework.container.SequenceBeforeSuite;
+import org.citrusframework.dsl.endpoint.CitrusEndpoints;
+import org.citrusframework.http.client.HttpClient;
+import org.citrusframework.simulator.sample.Simulator;
+import org.citrusframework.spi.Resources;
+import org.citrusframework.testng.spring.TestNGCitrusSpringSupport;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.test.context.ContextConfiguration;
+import org.testng.annotations.Test;
+
+@Test
+@ContextConfiguration(classes = ResponseGeneratorIT.EndpointConfig.class)
+public class ResponseGeneratorIT extends TestNGCitrusSpringSupport {
+
+ @Autowired
+ @Qualifier("pingClient")
+ private HttpClient pingClient;
+
+ @CitrusTest
+ public void shouldPerformDefaultOpenApiPingOperation() {
+ variable("id", "1234");
+
+ $(http().client(pingClient)
+ .send()
+ .put("/ping/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/ping.json")));
+
+ $(http().client(pingClient)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .header("Ping-Time", "@isNumber()@")
+ .validate(jsonPath()
+ .expression("$.pingCount", not(equalTo("1001")))));
+ }
+
+ @CitrusTest
+ public void shouldPerformSpecificApiPingOperation() {
+ long currentTime = System.currentTimeMillis();
+ long expectedPingLimit = currentTime - 1L;
+
+ variable("id", "5000");
+
+ $(http().client(pingClient)
+ .send()
+ .put("/ping/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/ping.json")));
+
+ $(http().client(pingClient)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .header("Ping-Time", "@isNumber()@")
+ .validate(jsonPath()
+ .expression("$.id", "5000")
+ .expression("$.pingCount", "@greaterThan("+expectedPingLimit+")@"))
+ );
+ }
+
+ @CitrusTest
+ public void shouldReturnPingTime0() {
+ variable("id", "15000");
+
+ $(http().client(pingClient)
+ .send()
+ .put("/ping/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/ping.json")));
+
+ $(http().client(pingClient)
+ .receive()
+ .response(HttpStatus.OK)
+ .message()
+ .header("Ping-Time", "0")
+ );
+ }
+
+ @CitrusTest
+ public void shouldFailOnBadRequest() {
+ variable("id", "10000");
+
+ $(http().client(pingClient)
+ .send()
+ .put("/ping/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/ping.json")));
+
+ $(http().client(pingClient)
+ .receive()
+ .response(HttpStatus.BAD_REQUEST)
+ .message().body("Requests with id == 10000 cannot be processed!"));
+ }
+
+ @CitrusTest
+ public void shouldFailOnUnsupportedType() {
+ variable("id", "10000");
+
+ $(http().client(pingClient)
+ .send()
+ .put("/ping/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_XML_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/ping.json")));
+
+ $(http().client(pingClient)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void shouldFailOnMissingPingTimeHeader() {
+ variable("id", "4000");
+
+ $(http().client(pingClient)
+ .send()
+ .put("/ping/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/ping.json")));
+
+ $(http().client(pingClient)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @Configuration
+ public static class EndpointConfig {
+
+ @Bean
+ public HttpClient pingClient() {
+ return CitrusEndpoints.http().client()
+ .requestUrl(String.format("http://localhost:%s/pingapi/v1", 8080))
+ .build();
+ }
+
+ @Bean
+ @ConditionalOnProperty(name = "simulator.mode", havingValue = "embedded")
+ public BeforeSuite startEmbeddedSimulator() {
+ return new SequenceBeforeSuite.Builder().actions(context -> SpringApplication.run(
+ Simulator.class)).build();
+ }
+ }
+}
diff --git a/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java
index 63807cf0e..0280ea2a0 100644
--- a/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java
+++ b/simulator-samples/sample-swagger/src/test/java/org/citrusframework/simulator/SimulatorSwaggerIT.java
@@ -16,6 +16,8 @@
package org.citrusframework.simulator;
+import static org.citrusframework.http.actions.HttpActionBuilder.http;
+
import org.citrusframework.annotations.CitrusTest;
import org.citrusframework.container.BeforeSuite;
import org.citrusframework.container.SequenceBeforeSuite;
@@ -36,8 +38,6 @@
import org.springframework.test.context.ContextConfiguration;
import org.testng.annotations.Test;
-import static org.citrusframework.http.actions.HttpActionBuilder.http;
-
/**
* @author Christoph Deppisch
*/
@@ -47,8 +47,8 @@ public class SimulatorSwaggerIT extends TestNGCitrusSpringSupport {
/** Test Http REST client */
@Autowired
- @Qualifier("petstoreClient")
- private HttpClient petstoreClient;
+ @Qualifier("petstoreClientV2")
+ private HttpClient petstoreClientV2;
/** Client to access simulator user interface */
@Autowired
@@ -56,7 +56,7 @@ public class SimulatorSwaggerIT extends TestNGCitrusSpringSupport {
private HttpClient simulatorUiClient;
@CitrusTest
- public void testUiInfo() {
+ public void uiInfoShouldSucceed() {
$(http().client(simulatorUiClient)
.send()
.get("/api/manage/info")
@@ -80,13 +80,13 @@ public void testUiInfo() {
}
@CitrusTest
- public void testAddPet() {
+ public void addPetShouldSucceed() {
variable("name", "hasso");
variable("category", "dog");
variable("tags", "huge");
variable("status", "pending");
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.send()
.post("/pet")
.message()
@@ -94,35 +94,80 @@ public void testAddPet() {
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(new Resources.ClasspathResource("templates/pet.json")));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK));
}
@CitrusTest
- public void testDeletePet() {
+ public void addPetShouldFailOnMissingName() {
+ variable("category", "dog");
+ variable("tags", "huge");
+ variable("status", "pending");
+
+ $(http().client(petstoreClientV2)
+ .send()
+ .post("/pet")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/pet_invalid.json")));
+
+ $(http().client(petstoreClientV2)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void deletePetShouldSucceed() {
variable("id", "citrus:randomNumber(10)");
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.send()
.delete("/pet/${id}"));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK));
}
@CitrusTest
- public void testGetPetById() {
+ public void deletePetShouldFailOnWrongIdFormat() {
+
+ $(http().client(petstoreClientV2)
+ .send()
+ .delete("/pet/xxxx"));
+
+ $(http().client(petstoreClientV2)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+// @CitrusTest
+// public void testDeletePetByOpenApi() {
+// variable("id", "citrus:randomNumber(10)");
+//$(openapi("Petstore/1.0.1").client(pingClient))
+// $(http().client(petstoreClient)
+// .send()
+// .delete("/pet/${id}"));
+//
+// $(http().client(petstoreClient)
+// .receive()
+// .response(HttpStatus.OK));
+// }
+
+ @CitrusTest
+ public void getPetByIdShouldSucceed() {
variable("id", "citrus:randomNumber(10)");
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.send()
.get("/pet/${id}")
.message()
+ .header("api_key", "xxx_api_key")
.accept(MediaType.APPLICATION_JSON_VALUE));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK)
.message()
@@ -131,13 +176,28 @@ public void testGetPetById() {
}
@CitrusTest
- public void testUpdatePet() {
+ public void getPetByIdShouldFailOnMissingApiKey() {
+ variable("id", "citrus:randomNumber(10)");
+
+ $(http().client(petstoreClientV2)
+ .send()
+ .get("/pet/${id}")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE));
+
+ $(http().client(petstoreClientV2)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void updatePetShouldSucceed() {
variable("name", "catty");
variable("category", "cat");
variable("tags", "cute");
variable("status", "sold");
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.send()
.put("/pet")
.message()
@@ -145,21 +205,21 @@ public void testUpdatePet() {
.contentType(MediaType.APPLICATION_JSON_VALUE)
.body(new Resources.ClasspathResource("templates/pet.json")));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK));
}
@CitrusTest
- public void testFindByStatus() {
- $(http().client(petstoreClient)
+ public void findByStatusShouldSucceed() {
+ $(http().client(petstoreClientV2)
.send()
.get("/pet/findByStatus")
.message()
.accept(MediaType.APPLICATION_JSON_VALUE)
.queryParam("status", "pending"));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK)
.message()
@@ -168,28 +228,28 @@ public void testFindByStatus() {
}
@CitrusTest
- public void testFindByStatusMissingQueryParameter() {
- $(http().client(petstoreClient)
+ public void findByStatusShouldFailOnMissingQueryParameter() {
+ $(http().client(petstoreClientV2)
.send()
.get("/pet/findByStatus")
.message()
.accept(MediaType.APPLICATION_JSON_VALUE));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.INTERNAL_SERVER_ERROR));
}
@CitrusTest
- public void testFindByTags() {
- $(http().client(petstoreClient)
+ public void findByTagsShouldSucceed() {
+ $(http().client(petstoreClientV2)
.send()
.get("/pet/findByTags")
.message()
.accept(MediaType.APPLICATION_JSON_VALUE)
.queryParam("tags", "huge,cute"));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK)
.message()
@@ -198,35 +258,53 @@ public void testFindByTags() {
}
@CitrusTest
- public void testPlaceOrder() {
- $(http().client(petstoreClient)
+ public void placeOrderShouldSucceed() {
+ $(http().client(petstoreClientV2)
.send()
.post("/store/order")
.message()
.accept(MediaType.APPLICATION_JSON_VALUE)
.contentType(MediaType.APPLICATION_JSON_VALUE)
- .body(new Resources.ClasspathResource("templates/order.json")));
+ .body(new Resources.ClasspathResource("templates/order.json"))
+ .header("api_key", "xxx_api_key")) ;
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK));
}
@CitrusTest
- public void testLoginUser() {
- $(http().client(petstoreClient)
+ public void placeOrderShouldFailOnInvalidDateFormat() {
+ $(http().client(petstoreClientV2)
+ .send()
+ .post("/store/order")
+ .message()
+ .accept(MediaType.APPLICATION_JSON_VALUE)
+ .contentType(MediaType.APPLICATION_JSON_VALUE)
+ .body(new Resources.ClasspathResource("templates/order_invalid_date.json"))
+ .header("api_key", "xxx_api_key")) ;
+
+ $(http().client(petstoreClientV2)
+ .receive()
+ .response(HttpStatus.INTERNAL_SERVER_ERROR));
+ }
+
+ @CitrusTest
+ public void loginUserShouldSucceed() {
+ $(http().client(petstoreClientV2)
.send()
.get("/user/login")
.queryParam("username", "citrus:randomString(10)")
.queryParam("password", "citrus:randomString(8)")
.message()
- .accept("text/plain"));
+ .header("api_key", "xxx_api_key")
+ .accept(MediaType.APPLICATION_JSON_VALUE));
- $(http().client(petstoreClient)
+ $(http().client(petstoreClientV2)
.receive()
.response(HttpStatus.OK)
.message()
- .type(MessageType.PLAINTEXT)
+ .type(MessageType.JSON)
.body("@notEmpty()@")
.header("X-Rate-Limit", "@isNumber()@")
.header("X-Expires-After", "@matchesDatePattern('yyyy-MM-dd'T'hh:mm:ss')@"));
@@ -236,7 +314,7 @@ public void testLoginUser() {
public static class EndpointConfig {
@Bean
- public HttpClient petstoreClient() {
+ public HttpClient petstoreClientV2() {
return CitrusEndpoints.http().client()
.requestUrl(String.format("http://localhost:%s/petstore/v2", 8080))
.build();
diff --git a/simulator-samples/sample-swagger/src/test/resources/templates/order.json b/simulator-samples/sample-swagger/src/test/resources/templates/order.json
index bfb074b83..6cd5271a5 100644
--- a/simulator-samples/sample-swagger/src/test/resources/templates/order.json
+++ b/simulator-samples/sample-swagger/src/test/resources/templates/order.json
@@ -2,7 +2,7 @@
"id": citrus:randomNumber(10),
"petId": citrus:randomNumber(10),
"quantity": 1,
- "shipDate": "citrus:currentDate('yyyy-MM-dd'T'hh:mm:ss')",
+ "shipDate": "citrus:currentDate('yyyy-MM-dd'T'HH:mm:ssZ')",
"status": "placed",
"complete": false
-}
\ No newline at end of file
+}
diff --git a/simulator-samples/sample-swagger/src/test/resources/templates/order_invalid_date.json b/simulator-samples/sample-swagger/src/test/resources/templates/order_invalid_date.json
new file mode 100644
index 000000000..a8a679b77
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/test/resources/templates/order_invalid_date.json
@@ -0,0 +1,8 @@
+{
+ "id": citrus:randomNumber(10),
+ "petId": citrus:randomNumber(10),
+ "quantity": 1,
+ "shipDate": "citrus:currentDate('yyyy-MM-dd'T'HH:mm')",
+ "status": "placed",
+ "complete": false
+}
diff --git a/simulator-samples/sample-swagger/src/test/resources/templates/pet_invalid.json b/simulator-samples/sample-swagger/src/test/resources/templates/pet_invalid.json
new file mode 100644
index 000000000..ef70f4ed0
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/test/resources/templates/pet_invalid.json
@@ -0,0 +1,14 @@
+{
+ "id": citrus:randomNumber(10),
+ "category": {
+ "id": citrus:randomNumber(10),
+ "name": "${category}"
+ },
+ "tags": [
+ {
+ "id": citrus:randomNumber(10),
+ "name": "${tags}"
+ }
+ ],
+ "status": "${status}"
+}
diff --git a/simulator-samples/sample-swagger/src/test/resources/templates/ping.json b/simulator-samples/sample-swagger/src/test/resources/templates/ping.json
new file mode 100644
index 000000000..045a4667b
--- /dev/null
+++ b/simulator-samples/sample-swagger/src/test/resources/templates/ping.json
@@ -0,0 +1,3 @@
+{
+ "server": "localhost"
+}
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
deleted file mode 100644
index 669816336..000000000
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/config/OpenApiScenarioIdGenerationMode.java
+++ /dev/null
@@ -1,25 +0,0 @@
-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 c375712d1..e2ce3785a 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,17 +16,13 @@
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;
@@ -37,7 +33,7 @@
@Setter
@ToString
@ConfigurationProperties(prefix = "citrus.simulator")
-public class SimulatorConfigurationProperties implements ApplicationContextAware, EnvironmentAware, InitializingBean {
+public class SimulatorConfigurationProperties implements EnvironmentAware, InitializingBean {
private static final Logger logger = LoggerFactory.getLogger(SimulatorConfigurationProperties.class);
@@ -50,8 +46,6 @@ public class SimulatorConfigurationProperties implements ApplicationContextAware
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.
*/
@@ -108,13 +102,6 @@ public class SimulatorConfigurationProperties implements ApplicationContextAware
*/
private String outboundJsonDictionary = "outbound-json-dictionary.properties";
- 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));
@@ -128,12 +115,4 @@ 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;
- }
}
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 8334077da..8747230e1 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,23 +16,22 @@
package org.citrusframework.simulator.http;
-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 jakarta.annotation.Nullable;
import java.util.concurrent.atomic.AtomicReference;
import lombok.Getter;
import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
-import org.citrusframework.message.Message;
+import org.citrusframework.http.message.HttpMessage;
import org.citrusframework.message.MessageHeaders;
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;
@@ -53,10 +52,6 @@ public class HttpOperationScenario extends AbstractSimulatorScenario {
private final OasOperation operation;
- private OasResponse response;
-
- private HttpStatus statusCode = HttpStatus.OK;
-
private JsonPathMappingDataDictionary inboundDataDictionary;
private JsonPathMappingDataDictionary outboundDataDictionary;
@@ -69,10 +64,6 @@ public HttpOperationScenario(String path, String scenarioId, OpenApiSpecificatio
this.openApiSpecification = openApiSpecification;
this.operation = operation;
this.httpResponseActionBuilderProvider = httpResponseActionBuilderProvider;
-
- // 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
@@ -83,11 +74,11 @@ public void run(ScenarioRunner scenario) {
OpenApiServerActionBuilder openApiServerActionBuilder = new OpenApiActionBuilder(
openApiSpecification).server(getScenarioEndpoint());
- Message receivedMessage = receive(scenario, openApiServerActionBuilder);
+ HttpMessage receivedMessage = receive(scenario, openApiServerActionBuilder);
respond(scenario, openApiServerActionBuilder, receivedMessage);
}
- private Message receive(ScenarioRunner scenario,
+ private HttpMessage receive(ScenarioRunner scenarioRunner,
OpenApiServerActionBuilder openApiServerActionBuilder) {
OpenApiServerRequestActionBuilder requestActionBuilder = openApiServerActionBuilder.receive(
@@ -101,34 +92,58 @@ private Message receive(ScenarioRunner scenario,
requestActionBuilder.message().dictionary(inboundDataDictionary);
}
- AtomicReference receivedMessage = new AtomicReference<>();
+ AtomicReference receivedMessage = new AtomicReference<>();
requestActionBuilder.getMessageProcessors().add(
- (message, context) -> receivedMessage.set(message));
+ (message, context) -> receivedMessage.set((HttpMessage)message));
// Verify incoming request
- scenario.$(requestActionBuilder);
+ scenarioRunner.$(requestActionBuilder);
return receivedMessage.get();
}
- private void respond(ScenarioRunner scenario,
- OpenApiServerActionBuilder openApiServerActionBuilder, Message receivedMessage) {
+ private void respond(ScenarioRunner scenarioRunner,
+ OpenApiServerActionBuilder openApiServerActionBuilder, HttpMessage receivedMessage) {
HttpServerResponseActionBuilder responseBuilder = null;
if (httpResponseActionBuilderProvider != null) {
- responseBuilder = httpResponseActionBuilderProvider.provideHttpServerResponseActionBuilder(operation, receivedMessage);
+ responseBuilder = httpResponseActionBuilderProvider.provideHttpServerResponseActionBuilder(scenarioRunner, this, receivedMessage);
+ }
+
+ if (responseBuilder == null) {
+ responseBuilder = createRandomMessageResponseBuilder(openApiServerActionBuilder, receivedMessage.getAccept());
}
- HttpStatus httpStatus = response != null && response.getStatusCode() != null ? HttpStatus.valueOf(Integer.parseInt(response.getStatusCode())) : HttpStatus.OK;
- responseBuilder = responseBuilder != null ? responseBuilder : openApiServerActionBuilder.send(
- operation.operationId, httpStatus);
+ scenarioRunner.$(responseBuilder);
+ }
+
+ /**
+ * Creates a builder that creates a random message based on OpenApi specification.
+ * @param openApiServerActionBuilder
+ * @return
+ */
+ private HttpServerResponseActionBuilder createRandomMessageResponseBuilder(
+ OpenApiServerActionBuilder openApiServerActionBuilder, String accept) {
+ HttpServerResponseActionBuilder responseBuilder;
+
+ OasResponse response = determineResponse(accept);
+ HttpStatus httpStatus = getStatusFromResponseOrDefault(response);
+ responseBuilder = openApiServerActionBuilder.send(operation.operationId, httpStatus, accept);
responseBuilder.message()
.status(httpStatus)
.header(MessageHeaders.MESSAGE_PREFIX + "generated", true);
- // Return generated response
- scenario.$(responseBuilder);
+ return responseBuilder;
+ }
+
+ private HttpStatus getStatusFromResponseOrDefault(@Nullable OasResponse response) {
+ return response != null && response.getStatusCode() != null ? HttpStatus.valueOf(
+ Integer.parseInt(response.getStatusCode())) : HttpStatus.OK;
+ }
+
+ OasResponse determineResponse(String accept) {
+ return OasModelHelper.getResponseForRandomGeneration(getOasDocument(), operation, null, accept).orElse(null);
}
/**
@@ -144,24 +159,6 @@ public String getMethod() {
return operation.getMethod() != null ? operation.getMethod().toUpperCase() : null;
}
- /**
- * Sets the response.
- *
- * @param response
- */
- public void setResponse(OasResponse response) {
- this.response = response;
- }
-
- /**
- * Sets the statusCode.
- *
- * @param statusCode
- */
- public void setStatusCode(HttpStatus statusCode) {
- this.statusCode = statusCode;
- }
-
/**
* Sets the inboundDataDictionary.
*
@@ -180,20 +177,4 @@ public void setOutboundDataDictionary(JsonPathMappingDataDictionary outboundData
this.outboundDataDictionary = outboundDataDictionary;
}
- /**
- * Retrieve a unique scenario id for the oas operation.
- *
- * @param openApiScenarioIdGenerationMode
- * @param path
- * @param oasOperation
- * @return
- */
- 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
index 5bdc6eec3..66a8335e0 100644
--- 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
@@ -1,10 +1,27 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.citrusframework.simulator.http;
+import org.citrusframework.CitrusInstanceManager;
+import org.citrusframework.context.SpringBeanReferenceResolver;
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;
+import org.citrusframework.spi.ReferenceResolver;
+import org.springframework.context.support.AbstractApplicationContext;
/**
* Registrar for HTTP operation scenarios based on an OpenAPI specification.
@@ -19,10 +36,13 @@ public class HttpOperationScenarioRegistrar implements OpenApiSpecificationProce
public void process(OpenApiSpecification openApiSpecification) {
HttpScenarioGenerator generator = new HttpScenarioGenerator(openApiSpecification);
- ApplicationContext applicationContext = SimulatorConfigurationProperties.getApplicationContext();
- if (applicationContext instanceof ConfigurableApplicationContext configurableApplicationContext) {
- generator.postProcessBeanFactory(configurableApplicationContext.getBeanFactory());
- }
+ CitrusInstanceManager.get().ifPresent(citrus -> {
+ ReferenceResolver referenceResolver = citrus.getCitrusContext().getReferenceResolver();
+ if (referenceResolver instanceof SpringBeanReferenceResolver springBeanReferenceResolver
+ && springBeanReferenceResolver.getApplicationContext() instanceof AbstractApplicationContext abstractApplicationContext) {
+ generator.postProcessBeanFactory(abstractApplicationContext.getBeanFactory());
+ }
+ });
}
}
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
index a68b230b5..c58dac60f 100644
--- 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
@@ -1,15 +1,32 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.citrusframework.simulator.http;
-import io.apicurio.datamodels.openapi.models.OasOperation;
import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
-import org.citrusframework.message.Message;
+import org.citrusframework.http.message.HttpMessage;
+import org.citrusframework.simulator.scenario.ScenarioRunner;
+import org.citrusframework.simulator.scenario.SimulatorScenario;
/**
- * Interface for providing an {@link HttpServerResponseActionBuilder} based on an OpenAPI operation and a received message.
+ * Interface for providing an {@link HttpServerResponseActionBuilder} based on a {@link SimulatorScenario} and a received message.
*/
public interface HttpResponseActionBuilderProvider {
- HttpServerResponseActionBuilder provideHttpServerResponseActionBuilder(OasOperation oasOperation,
- Message receivedMessage);
+ HttpServerResponseActionBuilder provideHttpServerResponseActionBuilder(
+ ScenarioRunner scenarioRunner, SimulatorScenario simulatorScenario, HttpMessage 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 d7462bbac..739940c64 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,6 +16,7 @@
package org.citrusframework.simulator.http;
+import static org.citrusframework.util.StringUtils.appendSegmentToUrlPath;
import static org.springframework.beans.factory.support.BeanDefinitionBuilder.genericBeanDefinition;
import io.apicurio.datamodels.combined.visitors.CombinedVisitorAdapter;
@@ -25,10 +26,10 @@
import io.apicurio.datamodels.openapi.models.OasPaths;
import jakarta.annotation.Nonnull;
import java.util.Map;
+import lombok.Getter;
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;
@@ -58,8 +59,15 @@ public class HttpScenarioGenerator implements BeanFactoryPostProcessor {
/**
* Optional context path
*/
+ @Getter
private String contextPath = "";
+ @Getter
+ private boolean requestValidationEnabled = true;
+
+ @Getter
+ private boolean responseValidationEnabled = true;
+
/**
* Constructor using Spring environment.
*/
@@ -86,75 +94,46 @@ public HttpScenarioGenerator(OpenApiSpecification openApiSpecification) {
this.openApiSpecification = openApiSpecification;
}
+ public void setRequestValidationEnabled(boolean enabled) {
+ this.requestValidationEnabled = enabled;
+ if (openApiSpecification != null) {
+ openApiSpecification.setRequestValidationEnabled(enabled);
+ }
+ }
+
+ public void setResponseValidationEnabled(boolean enabled) {
+ this.responseValidationEnabled = enabled;
+ if (openApiSpecification != null) {
+ openApiSpecification.setResponseValidationEnabled(enabled);
+ }
+ }
+
@Override
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);
+ initOpenApiSpecification();
}
- HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = retrieveOptionalBuilderProvider(
- beanFactory);
-
- OpenApiScenarioIdGenerationMode openApiScenarioIdGenerationMode = beanFactory.getBean(
- SimulatorRestConfigurationProperties.class).getOpenApiScenarioIdGenerationMode();
-
TestContext testContext = new TestContext();
OasDocument openApiDocument = openApiSpecification.getOpenApiDoc(testContext);
if (openApiDocument != null && openApiDocument.paths != null) {
- openApiDocument.paths.accept(new CombinedVisitorAdapter() {
-
- @Override
- public void visitPaths(OasPaths oasPaths) {
- oasPaths.getPathItems().forEach(oasPathItem -> oasPathItem.accept(this));
- }
-
- @Override
- public void visitPathItem(OasPathItem oasPathItem) {
- String path = oasPathItem.getPath();
- for (Map.Entry operationEntry : OasModelHelper.getOperationMap(
- oasPathItem).entrySet()) {
-
- 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);
-
- 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));
- }
- }
- }
- });
+ openApiDocument.paths.accept(new ScenarioRegistrar(beanFactory));
}
}
+ private void initOpenApiSpecification() {
+ 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);
+ openApiSpecification.setResponseValidationEnabled(responseValidationEnabled);
+ openApiSpecification.setRequestValidationEnabled(requestValidationEnabled);
+ }
+
private static HttpResponseActionBuilderProvider retrieveOptionalBuilderProvider(
ConfigurableListableBeanFactory beanFactory) {
HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = null;
@@ -180,10 +159,6 @@ protected HttpOperationScenario createScenario(String path, String scenarioId, O
return new HttpOperationScenario(path, scenarioId, openApiSpecification, operation, httpResponseActionBuilderProvider);
}
- public String getContextPath() {
- return contextPath;
- }
-
public void setContextPath(String contextPath) {
this.contextPath = contextPath;
@@ -191,4 +166,60 @@ public void setContextPath(String contextPath) {
openApiSpecification.setRootContextPath(contextPath);
}
}
+
+ private class ScenarioRegistrar extends CombinedVisitorAdapter {
+
+ private final ConfigurableListableBeanFactory beanFactory;
+
+ private ScenarioRegistrar(ConfigurableListableBeanFactory beanFactory) {
+ this.beanFactory = beanFactory;
+ }
+
+ @Override
+ public void visitPaths(OasPaths oasPaths) {
+ oasPaths.getPathItems().forEach(oasPathItem -> oasPathItem.accept(this));
+ }
+
+ @Override
+ public void visitPathItem(OasPathItem oasPathItem) {
+
+ HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = retrieveOptionalBuilderProvider(
+ beanFactory);
+
+ OasDocument oasDocument = openApiSpecification.getOpenApiDoc(null);
+ String path = oasPathItem.getPath();
+ for (Map.Entry operationEntry : OasModelHelper.getOperationMap(
+ oasPathItem).entrySet()) {
+
+ String fullPath = appendSegmentToUrlPath(appendSegmentToUrlPath(openApiSpecification.getRootContextPath(), OasModelHelper.getBasePath(oasDocument)), path);
+ OasOperation oasOperation = operationEntry.getValue();
+
+ String scenarioId = openApiSpecification.getUniqueId(oasOperation);
+
+ if (beanFactory instanceof BeanDefinitionRegistry beanDefinitionRegistry) {
+ logger.info("Register auto generated scenario as bean definition: {}", fullPath);
+
+ 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));
+ }
+ }
+ }
+ }
}
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 eeaeffd73..951f74491 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
@@ -24,7 +24,6 @@
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;
@@ -51,11 +50,6 @@ public class SimulatorRestConfigurationProperties implements InitializingBean {
*/
private List urlMappings = List.of("/services/rest/**");
- /**
- * The scenario id generation mode for open api scenario generation.
- */
- private OpenApiScenarioIdGenerationMode openApiScenarioIdGenerationMode = OpenApiScenarioIdGenerationMode.FULL_PATH;
-
/**
* The OpenApi used by the simulator to simulate OpenApi operations.
*/
@@ -93,12 +87,12 @@ public String toString() {
.toString();
}
-
@Getter
@Setter
public static class OpenApi {
private String api;
private String contextPath;
private boolean enabled = false;
+ private String alias;
}
}
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
index 91f12d81b..f74e38aeb 100644
--- 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
@@ -1,20 +1,38 @@
+/*
+ * Copyright the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
package org.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 io.apicurio.datamodels.openapi.models.OasResponse;
import java.io.IOException;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Stream;
+import lombok.Getter;
+import org.citrusframework.context.SpringBeanReferenceResolver;
import org.citrusframework.context.TestContext;
import org.citrusframework.exceptions.CitrusRuntimeException;
import org.citrusframework.exceptions.TestCaseFailedException;
+import org.citrusframework.exceptions.ValidationException;
import org.citrusframework.functions.DefaultFunctionRegistry;
import org.citrusframework.http.actions.HttpServerResponseActionBuilder;
import org.citrusframework.http.message.HttpMessage;
@@ -23,7 +41,8 @@
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.IntegrationTest;
+import org.citrusframework.simulator.http.HttpOperationScenarioIT.HttpOperationScenarioTestConfiguration;
import org.citrusframework.simulator.scenario.ScenarioEndpoint;
import org.citrusframework.simulator.scenario.ScenarioEndpointConfiguration;
import org.citrusframework.simulator.scenario.ScenarioRunner;
@@ -35,68 +54,60 @@
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.boot.test.context.TestConfiguration;
+import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.Bean;
import org.springframework.http.HttpHeaders;
+import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.util.ReflectionTestUtils;
-@ExtendWith(MockitoExtension.class)
+@IntegrationTest
+@ContextConfiguration(classes = HttpOperationScenarioTestConfiguration.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() {
+ void beforeEach(ApplicationContext applicationContext) {
+ defaultListableBeanFactory = (DefaultListableBeanFactory) ((ConfigurableApplicationContext)applicationContext).getBeanFactory();
testContext = new TestContext();
- testContext.setReferenceResolver(mock());
+ testContext.setReferenceResolver(new SpringBeanReferenceResolver(applicationContext));
testContext.setMessageValidatorRegistry(new DefaultMessageValidatorRegistry());
testContext.setFunctionRegistry(new DefaultFunctionRegistry());
testContext.setValidationMatcherRegistry(new DefaultValidationMatcherRegistry());
testContext.setLogModifier(new DefaultLogModifier());
- scenarioRunner = new ScenarioRunner(scenarioEndpoint, mock(), testContext);
+
+ scenarioRunner = new ScenarioRunner(scenarioEndpoint, applicationContext, 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_addPet_payloadValidationFailure", "POST_/petstore/v2/pet", "data/addPet_incorrect.json", IDENTITY, "OpenApi request validation failed for operation: /post/pet (addPet)\n"
+ + "\tERROR - Object instance has properties which are not allowed by the schema: [\"wrong_id_property\"]: []"),
+ arguments("v3_addPet_payloadValidationFailure", "POST_/petstore/v3/pet", "data/addPet_incorrect.json", IDENTITY, "OpenApi request validation failed for operation: /post/pet (addPet)\n"
+ + "\tERROR - Object instance has properties which are not allowed by the schema: [\"wrong_id_property\"]: []"),
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]+'")
+ arguments("v2_getPetById_pathParameterValidationFailure", "GET_/petstore/v2/pet/{petId}", null, (Function)(text) -> text.replace("{petId}", "xxxx"), "OpenApi request validation failed for operation: /get/pet/{petId} (getPetById)\n"
+ + "\tERROR - Instance type (string) does not match any allowed primitive type (allowed: [\"integer\"]): []"),
+ arguments("v3_getPetById_pathParameterValidationFailure", "GET_/petstore/v3/pet/{petId}", null, (Function)(text) -> text.replace("{petId}", "xxxx"), "OpenApi request validation failed for operation: /get/pet/{petId} (getPetById)\n"
+ + "\tERROR - Instance type (string) does not match any allowed primitive type (allowed: [\"integer\"]): []")
);
}
@@ -110,7 +121,9 @@ void scenarioExecution(String name, String operationName, String payloadFile, Fu
HttpOperationScenario httpOperationScenario = getHttpOperationScenario(operationName);
HttpMessage controlMessage = new HttpMessage();
- OpenApiClientResponseActionBuilder.fillMessageFromResponse(httpOperationScenario.getOpenApiSpecification(), testContext, controlMessage, httpOperationScenario.getOperation(), httpOperationScenario.getResponse());
+ OasResponse oasResponse = httpOperationScenario.determineResponse(null);
+ OpenApiClientResponseActionBuilder.fillMessageFromResponse(httpOperationScenario.getOpenApiSpecification(),
+ testContext, controlMessage, httpOperationScenario.getOperation(), oasResponse);
this.scenarioExecution(operationName, payloadFile, urlAdjuster, exceptionMessage, controlMessage);
}
@@ -120,7 +133,7 @@ void scenarioExecution(String name, String operationName, String payloadFile, Fu
void scenarioExecutionWithProvider(String name, String operationName, String payloadFile, Function urlAdjuster, String exceptionMessage) {
String payload = "{\"id\":1234}";
- HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = (oasOperation, receivedMessage) -> {
+ HttpResponseActionBuilderProvider httpResponseActionBuilderProvider = (scenarioRunner, simulatorScenario, receivedMessage) -> {
HttpServerResponseActionBuilder serverResponseActionBuilder = new HttpServerResponseActionBuilder();
serverResponseActionBuilder
.endpoint(scenarioEndpoint)
@@ -179,7 +192,7 @@ private void scenarioExecution(String operationName, String payloadFile, Functio
if (exceptionMessage != null) {
assertThatThrownBy(() -> httpOperationScenario.run(scenarioRunner)).isInstanceOf(
- TestCaseFailedException.class).hasMessage(exceptionMessage);
+ TestCaseFailedException.class).cause().isInstanceOf(ValidationException.class).hasMessage(exceptionMessage);
} else {
assertThatCode(() -> httpOperationScenario.run(scenarioRunner)).doesNotThrowAnyException();
@@ -206,6 +219,7 @@ private static class DirectScenarioEndpoint extends ScenarioEndpoint {
private Message receiveMessage;
+ @Getter
private Message sendMessage;
public DirectScenarioEndpoint() {
@@ -236,9 +250,16 @@ public void setReceiveMessage(Message receiveMessage) {
this.receiveMessage = receiveMessage;
}
- public Message getSendMessage() {
- return sendMessage;
- }
+ }
+ @TestConfiguration
+ public static class HttpOperationScenarioTestConfiguration {
+
+ @Bean
+ public OpenApiRepository repository() {
+ OpenApiRepository openApiRepository = new OpenApiRepository();
+ openApiRepository.setLocations(List.of("swagger/petstore-v2.json", "swagger/petstore-v3.json"));
+ return openApiRepository;
+ }
}
}
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 41682e562..ecc6de497 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
@@ -83,8 +83,6 @@ void testGetMappingKey(String version) {
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),
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 553c4bb57..8530a2449 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
@@ -169,7 +169,6 @@ void testGenerateScenariosWithDataDictionariesAtRootContext(String version) {
}
private void mockBeanFactory(BeanFactory beanFactory) {
- doReturn(new SimulatorRestConfigurationProperties()).when(beanFactory).getBean(SimulatorRestConfigurationProperties.class);
doThrow(new BeansException("No such bean") {
}).when(beanFactory).getBean(HttpResponseActionBuilderProvider.class);
}