diff --git a/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/FailScenario.java b/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/FailScenario.java
index 3e0d04f82..b4d604ca3 100644
--- a/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/FailScenario.java
+++ b/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/FailScenario.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * 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.
@@ -16,33 +16,33 @@
package org.citrusframework.simulator.sample.scenario;
-import static org.citrusframework.actions.EchoAction.Builder.echo;
-import static org.citrusframework.actions.FailAction.Builder.fail;
-import static org.citrusframework.dsl.MessageSupport.MessageBodySupport.fromBody;
-
import org.citrusframework.simulator.scenario.AbstractSimulatorScenario;
import org.citrusframework.simulator.scenario.Scenario;
import org.citrusframework.simulator.scenario.ScenarioRunner;
-import org.springframework.http.HttpStatus;
+import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
+import static org.citrusframework.actions.EchoAction.Builder.echo;
+import static org.citrusframework.actions.FailAction.Builder.fail;
+
/**
- * @author Christoph Deppisch
+ * This scenario fails expectantly, using the {@link org.citrusframework.actions.FailAction.Builder#fail(String)}
+ * method. From the view point of a {@link SimulatorScenario}, there is nothing wrong with it. It should therefore be
+ * viewed as a "successful simulation".
+ *
+ * In contrary to this, the {@link ThrowScenario} does fail in an "uncontrolled" manner (by throwing an exception at
+ * runtime), therefore results in a "failed simulation".
*/
@Scenario("Fail")
-@RequestMapping(value = "/services/rest/simulator/fail", method = RequestMethod.POST)
+@RequestMapping(value = "/services/rest/simulator/fail", method = RequestMethod.GET)
public class FailScenario extends AbstractSimulatorScenario {
@Override
public void run(ScenarioRunner scenario) {
scenario.$(scenario.http()
.receive()
- .post()
- .message()
- .body("" +
- "Fail!" +
- ""));
+ .get());
scenario.$(echo("Careful - I am gonna fail successfully (:"));
diff --git a/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/ThrowScenario.java b/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/ThrowScenario.java
new file mode 100644
index 000000000..dd4130017
--- /dev/null
+++ b/simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/ThrowScenario.java
@@ -0,0 +1,48 @@
+/*
+ * 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.sample.scenario;
+
+import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.simulator.scenario.AbstractSimulatorScenario;
+import org.citrusframework.simulator.scenario.Scenario;
+import org.citrusframework.simulator.scenario.ScenarioRunner;
+import org.citrusframework.simulator.scenario.SimulatorScenario;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+
+import static org.citrusframework.actions.EchoAction.Builder.echo;
+
+/**
+ * This scenario fails at runtime, unexpectedly. It must therefore be reported as failed {@link SimulatorScenario}.
+ *
+ * On the other hand, if a simulation fails on purpose, see {@link FailScenario}, it must be a "successful simulation".
+ */
+@Scenario("Throw")
+@RequestMapping(value = "/services/rest/simulator/throw", method = RequestMethod.GET)
+public class ThrowScenario extends AbstractSimulatorScenario {
+
+ @Override
+ public void run(ScenarioRunner scenario) {
+ scenario.$(scenario.http()
+ .receive()
+ .get());
+
+ scenario.$(echo("Careful - I am gonna fail (:"));
+
+ throw new CitrusRuntimeException("Every failure is a step to success.");
+ }
+}
diff --git a/simulator-samples/sample-rest/src/test/java/org/citrusframework/simulator/SimulatorRestIT.java b/simulator-samples/sample-rest/src/test/java/org/citrusframework/simulator/SimulatorRestIT.java
index 71ff10a94..6ad8d6fa9 100644
--- a/simulator-samples/sample-rest/src/test/java/org/citrusframework/simulator/SimulatorRestIT.java
+++ b/simulator-samples/sample-rest/src/test/java/org/citrusframework/simulator/SimulatorRestIT.java
@@ -227,22 +227,51 @@ public void testInterveningRequest() {
}
/**
- * Sends a request to the server expecting it to purposefully fail a simulation.
+ * Sends a request to the server, expecting it to purposefully fail a simulation. The response code must therefore
+ * be {@link HttpStatus#OK}.
+ *
+ * @see org.citrusframework.simulator.sample.scenario.FailScenario
*/
@CitrusTest
- public void testFailingSimulation() {
+ public void testSimulationFailingExpectantly() {
$(http().client(simulatorClient)
.send()
- .post("fail")
+ .get("fail"));
+
+ $(http().client(simulatorClient)
+ .receive()
+ .response(HttpStatus.OK));
+ }
+
+ /**
+ * Sends a request to the server, expecting it to execute a simulation. The response should indicate the unexpected
+ * error, returning a {@link HttpStatus#INTERNAL_SERVER_ERROR}.
+ *
+ * @see org.citrusframework.simulator.sample.scenario.ThrowScenario
+ */
+ @CitrusTest
+ public void testSimulationWithUnexpectedError() {
+ $(http().client(simulatorClient)
+ .send()
+ .get("throw")
.message()
- .contentType(MediaType.APPLICATION_XML_VALUE)
- .body("" +
- "Fail!" +
- ""));
+ .accept(MediaType.APPLICATION_JSON_VALUE));
$(http().client(simulatorClient)
.receive()
- .response(HttpStatus.OK)); // TODO: Pretty sure this should be HttpStatus.INTERNAL_SERVER_ERROR
+ .response(HttpStatus.INTERNAL_SERVER_ERROR)
+ .message()
+ .body(
+ // language=json
+ """
+ {
+ "timestamp":"@ignore@",
+ "status":555,
+ "error":"Http Status 555",
+ "path":"/services/rest/simulator/throw"
+ }
+ """
+ ));
}
@Configuration
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyException.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyException.java
new file mode 100644
index 000000000..e133bc795
--- /dev/null
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyException.java
@@ -0,0 +1,41 @@
+/*
+ * 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.endpoint;
+
+import org.citrusframework.message.DefaultMessage;
+
+/**
+ * An implementation of a {@link org.citrusframework.message.Message} that hints that a simulation has failed. Because
+ * of the nature of {@link java.util.concurrent.CompletableFuture}'s, there is no
+ * other way than mapping the response message in case of an error. Exception-propagation from the asynchronous thread
+ * back into the parent does not exist.
+ *
+ * @see SimulatorEndpointAdapter
+ */
+public class SimulationFailedUnexpectedlyException extends DefaultMessage {
+
+ public static final String EXCEPTION_TYPE = SimulationFailedUnexpectedlyException.class.getSimpleName() + ":Exception";
+
+ public SimulationFailedUnexpectedlyException(Throwable e) {
+ super(e);
+ }
+
+ @Override
+ public String getType() {
+ return EXCEPTION_TYPE;
+ }
+}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapter.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapter.java
index d1e215b2a..6fcbc7799 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapter.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapter.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -16,6 +16,8 @@
package org.citrusframework.simulator.endpoint;
+import lombok.Getter;
+import lombok.Setter;
import org.citrusframework.endpoint.adapter.RequestDispatchingEndpointAdapter;
import org.citrusframework.message.Message;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
@@ -26,130 +28,109 @@
import org.citrusframework.simulator.service.ScenarioExecutorService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationContextAware;
-import org.springframework.util.StringUtils;
+import org.springframework.web.server.ResponseStatusException;
-import java.util.Collections;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
-import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-/**
- * @author Christoph Deppisch
- */
-public class SimulatorEndpointAdapter extends RequestDispatchingEndpointAdapter implements ApplicationContextAware {
+import static java.lang.String.format;
+import static java.lang.Thread.currentThread;
+import static java.util.Collections.emptyList;
+import static java.util.Objects.nonNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static org.citrusframework.simulator.endpoint.SimulationFailedUnexpectedlyException.EXCEPTION_TYPE;
+import static org.citrusframework.util.StringUtils.hasText;
- private static final Logger logger = LoggerFactory.getLogger(SimulatorEndpointAdapter.class);
+public class SimulatorEndpointAdapter extends RequestDispatchingEndpointAdapter {
- @Autowired
- private CorrelationHandlerRegistry handlerRegistry;
+ private static final Logger logger = LoggerFactory.getLogger(SimulatorEndpointAdapter.class);
- @Autowired
- private SimulatorConfigurationProperties configuration;
+ private final ApplicationContext applicationContext;
+ private final CorrelationHandlerRegistry handlerRegistry;
+ private final ScenarioExecutorService scenarioExecutorService;
+ private final SimulatorConfigurationProperties configuration;
- @Autowired
- private ScenarioExecutorService scenarioExecutorService;
+ @Getter
+ @Setter
+ private boolean handleResponse = true;
- /**
- * Spring application context
- */
- private ApplicationContext applicationContext;
+ public SimulatorEndpointAdapter(ApplicationContext applicationContext, CorrelationHandlerRegistry handlerRegistry, ScenarioExecutorService scenarioExecutorService, SimulatorConfigurationProperties configuration) {
+ this.applicationContext = applicationContext;
+ this.handlerRegistry = handlerRegistry;
+ this.scenarioExecutorService = scenarioExecutorService;
+ this.configuration = configuration;
+ }
- /**
- * When adapter is asynchronous response handling is skipped
- */
- private boolean handleResponse = true;
+ private static ResponseStatusException getResponseStatusException(Throwable e) {
+ return new ResponseStatusException(555, "Simulation failed with an Exception!", e);
+ }
@Override
- protected Message handleMessageInternal(Message request) {
- CorrelationHandler handler = handlerRegistry.findHandlerFor(request);
- if (handler != null) {
- CompletableFuture responseFuture = new CompletableFuture<>();
- handler.getScenarioEndpoint().add(request, responseFuture);
-
- try {
- if (handleResponse) {
- return responseFuture.get(configuration.getDefaultTimeout(), TimeUnit.MILLISECONDS);
- } else {
- return null;
- }
- } catch (TimeoutException e) {
- logger.warn(String.format("No response for scenario '%s'", handler.getScenarioEndpoint().getName()));
- return null;
- } catch (InterruptedException e) {
- Thread.currentThread().interrupt();
- throw new SimulatorException(e);
- } catch (ExecutionException e) {
- throw new SimulatorException(e);
- }
+ protected Message handleMessageInternal(Message message) {
+ CorrelationHandler handler = handlerRegistry.findHandlerFor(message);
+
+ if (nonNull(handler)) {
+ return handleMessageWithCorrelation(message, handler);
} else {
- return super.handleMessageInternal(request);
+ return super.handleMessageInternal(message);
}
}
+ private Message handleMessageWithCorrelation(Message request, CorrelationHandler handler) {
+ CompletableFuture responseFuture = new CompletableFuture<>();
+ handler.getScenarioEndpoint().add(request, responseFuture);
+
+ return awaitResponseOrThrowException(responseFuture, handler.getScenarioEndpoint().getName());
+ }
+
@Override
- public Message dispatchMessage(Message request, String mappingName) {
+ public Message dispatchMessage(Message message, String mappingName) {
String scenarioName = mappingName;
- CompletableFuture responseFuture = new CompletableFuture<>();
+
SimulatorScenario scenario;
- if (StringUtils.hasText(scenarioName) && applicationContext.containsBean(scenarioName)) {
- scenario = applicationContext.getBean(scenarioName, SimulatorScenario.class);
- } else {
+ if (!hasText(scenarioName) || !applicationContext.containsBean(scenarioName)) {
scenarioName = configuration.getDefaultScenario();
- logger.info("Unable to find scenario for mapping '{}' - " +
- "using default scenario '{}'", mappingName, scenarioName);
- scenario = applicationContext.getBean(scenarioName, SimulatorScenario.class);
+ logger.info("Unable to find scenario for mapping '{}' - using default scenario '{}'", mappingName, scenarioName);
}
+ scenario = applicationContext.getBean(scenarioName, SimulatorScenario.class);
scenario.getScenarioEndpoint().setName(scenarioName);
- scenario.getScenarioEndpoint().add(request, responseFuture);
- scenarioExecutorService.run(scenario, scenarioName, Collections.emptyList());
+
+ CompletableFuture responseFuture = new CompletableFuture<>();
+ scenario.getScenarioEndpoint().add(message, responseFuture);
+ try {
+ scenarioExecutorService.run(scenario, scenarioName, emptyList());
+ } catch (Exception e) {
+ throw getResponseStatusException(e);
+ }
+
+ return awaitResponseOrThrowException(responseFuture, scenarioName);
+ }
+
+ private Message awaitResponseOrThrowException(CompletableFuture responseFuture, String scenarioName) {
try {
if (handleResponse) {
- return responseFuture.get(configuration.getDefaultTimeout(), TimeUnit.MILLISECONDS);
+ var message = responseFuture.get(configuration.getDefaultTimeout(), MILLISECONDS);
+
+ if (EXCEPTION_TYPE.equals(message.getType())) {
+ throw getResponseStatusException(message.getPayload(Throwable.class));
+ }
+
+ return message;
} else {
return null;
}
} catch (TimeoutException e) {
- logger.warn(String.format("No response for scenario '%s'", scenarioName));
+ logger.warn(format("No response for scenario '%s'", scenarioName));
return null;
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ currentThread().interrupt();
throw new SimulatorException(e);
} catch (ExecutionException e) {
throw new SimulatorException(e);
}
}
-
- /**
- * Sets the applicationContext.
- *
- * @param applicationContext
- */
- @Override
- public void setApplicationContext(ApplicationContext applicationContext) {
- this.applicationContext = applicationContext;
- }
-
- /**
- * Gets the handleResponse.
- *
- * @return
- */
- public boolean isHandleResponse() {
- return handleResponse;
- }
-
- /**
- * Sets the handleResponse.
- *
- * @param handleResponse
- */
- public void setHandleResponse(boolean handleResponse) {
- this.handleResponse = handleResponse;
- }
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAutoConfiguration.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAutoConfiguration.java
index 8abd2e121..c808de2f7 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAutoConfiguration.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAutoConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -16,15 +16,20 @@
package org.citrusframework.simulator.endpoint;
+import jakarta.annotation.Nullable;
import org.citrusframework.channel.ChannelSyncEndpoint;
import org.citrusframework.channel.ChannelSyncEndpointConfiguration;
+import org.citrusframework.context.TestContextFactory;
import org.citrusframework.endpoint.Endpoint;
import org.citrusframework.endpoint.EndpointAdapter;
import org.citrusframework.endpoint.adapter.EmptyResponseEndpointAdapter;
import org.citrusframework.simulator.SimulatorAutoConfiguration;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.citrusframework.simulator.correlation.CorrelationHandlerRegistry;
import org.citrusframework.simulator.scenario.mapper.ContentBasedXPathScenarioMapper;
import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
+import org.citrusframework.simulator.service.ScenarioExecutorService;
+import org.citrusframework.simulator.ws.SoapMessageHelper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -32,22 +37,27 @@
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
-/**
- * @author Christoph Deppisch
- */
+import static java.util.Objects.nonNull;
+
@Configuration
@AutoConfigureAfter(SimulatorAutoConfiguration.class)
@ConditionalOnProperty(prefix = "citrus.simulator.endpoint", value = "enabled", havingValue = "true")
public class SimulatorEndpointAutoConfiguration {
- @Autowired(required = false)
- private SimulatorEndpointComponentConfigurer configurer;
+ private final ApplicationContext applicationContext;
+ private final SimulatorConfigurationProperties simulatorConfiguration;
+
+ private @Nullable SimulatorEndpointComponentConfigurer configurer;
+
+ public SimulatorEndpointAutoConfiguration(ApplicationContext applicationContext, SimulatorConfigurationProperties simulatorConfiguration, @Autowired(required = false) @Nullable SimulatorEndpointComponentConfigurer configurer) {
+ this.applicationContext = applicationContext;
+ this.simulatorConfiguration = simulatorConfiguration;
+ this.configurer = configurer;
+ }
- @Autowired
- private SimulatorConfigurationProperties simulatorConfiguration;
@Bean
- protected Endpoint simulatorEndpoint(ApplicationContext applicationContext) {
+ protected Endpoint simulatorEndpoint() {
if (configurer != null) {
return configurer.endpoint(applicationContext);
} else {
@@ -60,8 +70,8 @@ protected Endpoint simulatorEndpoint(ApplicationContext applicationContext) {
}
@Bean
- public SimulatorEndpointAdapter simulatorEndpointAdapter() {
- return new SimulatorEndpointAdapter();
+ public SimulatorEndpointAdapter simulatorEndpointAdapter(CorrelationHandlerRegistry handlerRegistry, ScenarioExecutorService scenarioExecutorService, SimulatorConfigurationProperties configuration) {
+ return new SimulatorEndpointAdapter(applicationContext, handlerRegistry, scenarioExecutorService, configuration);
}
@Bean
@@ -74,31 +84,29 @@ public ScenarioMapper simulatorScenarioMapper() {
}
@Bean
- public SimulatorEndpointPoller simulatorEndpointPoller(ApplicationContext applicationContext) {
+ public SimulatorEndpointPoller simulatorEndpointPoller(SimulatorEndpointAdapter simulatorEndpointAdapter, SoapMessageHelper soapMessageHelper, TestContextFactory testContextFactory) {
SimulatorEndpointPoller endpointPoller;
if (configurer != null && configurer.useSoapEnvelope()) {
- endpointPoller = new SimulatorSoapEndpointPoller();
+ endpointPoller = new SimulatorSoapEndpointPoller(testContextFactory, soapMessageHelper);
} else {
- endpointPoller = new SimulatorEndpointPoller();
+ endpointPoller = new SimulatorEndpointPoller(testContextFactory);
}
- endpointPoller.setInboundEndpoint(simulatorEndpoint(applicationContext));
- SimulatorEndpointAdapter endpointAdapter = simulatorEndpointAdapter();
- endpointAdapter.setApplicationContext(applicationContext);
- endpointAdapter.setMappingKeyExtractor(simulatorScenarioMapper());
- endpointAdapter.setFallbackEndpointAdapter(simulatorFallbackEndpointAdapter());
+ endpointPoller.setInboundEndpoint(simulatorEndpoint());
+ simulatorEndpointAdapter.setMappingKeyExtractor(simulatorScenarioMapper());
+ simulatorEndpointAdapter.setFallbackEndpointAdapter(simulatorFallbackEndpointAdapter());
endpointPoller.setExceptionDelay(exceptionDelay());
- endpointPoller.setEndpointAdapter(endpointAdapter);
+ endpointPoller.setEndpointAdapter(simulatorEndpointAdapter);
return endpointPoller;
}
@Bean
public EndpointAdapter simulatorFallbackEndpointAdapter() {
- if (configurer != null) {
+ if (nonNull(configurer)) {
return configurer.fallbackEndpointAdapter();
}
@@ -110,7 +118,7 @@ public EndpointAdapter simulatorFallbackEndpointAdapter() {
* @return
*/
protected Long exceptionDelay() {
- if (configurer != null) {
+ if (nonNull(configurer)) {
return configurer.exceptionDelay(simulatorConfiguration);
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointPoller.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointPoller.java
index 981a3c3d9..ccc8db336 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointPoller.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorEndpointPoller.java
@@ -38,20 +38,16 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
-/**
- * @author Christoph Deppisch
- */
+import static java.lang.Thread.currentThread;
+import static java.util.concurrent.Executors.newSingleThreadExecutor;
+
@Setter
public class SimulatorEndpointPoller implements InitializingBean, Runnable, DisposableBean, ApplicationListener {
- /**
- * Logger
- */
private static final Logger logger = LoggerFactory.getLogger(SimulatorEndpointPoller.class);
private final ThreadFactory threadFactory = new ThreadFactoryBuilder()
@@ -60,17 +56,16 @@ public class SimulatorEndpointPoller implements InitializingBean, Runnable, Disp
.build();
/**
- * Thread running the server
+ * Thread running the server.
*/
- private final ExecutorService taskExecutor = Executors.newSingleThreadExecutor(threadFactory);
+ private final ExecutorService taskExecutor = newSingleThreadExecutor(threadFactory);
/**
* Running flag
*/
private final CompletableFuture running = new CompletableFuture<>();
- @Autowired
- private TestContextFactory testContextFactory;
+ private final TestContextFactory testContextFactory;
/**
* Endpoint destination that is constantly polled for new messages.
@@ -78,19 +73,23 @@ public class SimulatorEndpointPoller implements InitializingBean, Runnable, Disp
private Endpoint inboundEndpoint;
/**
- * Message handler for incoming simulator request messages
+ * Message handler for incoming simulator request messages.
*/
private EndpointAdapter endpointAdapter;
/**
- * Should automatically start on system load
+ * Flag indicating if the poller should automatically start on system startup.
*/
private boolean autoStart = true;
/**
* Polling delay after uncategorized exception occurred.
*/
- private long exceptionDelay = 10000L;
+ private long exceptionDelay = 5000L;
+
+ public SimulatorEndpointPoller(TestContextFactory testContextFactory) {
+ this.testContextFactory = testContextFactory;
+ }
@Override
public void run() {
@@ -181,11 +180,14 @@ public void stop() {
running.complete(false);
try {
- taskExecutor.awaitTermination(exceptionDelay, TimeUnit.MILLISECONDS);
- logger.info("Simulator endpoint poller termination complete");
+ if (!taskExecutor.awaitTermination(exceptionDelay, TimeUnit.MILLISECONDS)) {
+ logger.error("Could not terminate endpoint poller in timeout of {} ms!", exceptionDelay);
+ } else {
+ logger.info("Simulator endpoint poller termination complete");
+ }
} catch (InterruptedException e) {
logger.error("Error while waiting termination of endpoint poller", e);
- Thread.currentThread().interrupt();
+ currentThread().interrupt();
throw new SimulatorException(e);
} finally {
taskExecutor.shutdownNow();
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorSoapEndpointPoller.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorSoapEndpointPoller.java
index 284036df4..f015ba965 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorSoapEndpointPoller.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulatorSoapEndpointPoller.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -16,19 +16,21 @@
package org.citrusframework.simulator.endpoint;
+import org.citrusframework.context.TestContextFactory;
import org.citrusframework.message.Message;
import org.citrusframework.simulator.exception.SimulatorException;
import org.citrusframework.simulator.ws.SoapMessageHelper;
import org.citrusframework.ws.message.SoapMessage;
-import org.springframework.beans.factory.annotation.Autowired;
-/**
- * @author Christoph Deppisch
- */
public class SimulatorSoapEndpointPoller extends SimulatorEndpointPoller {
- @Autowired
- private SoapMessageHelper soapMessageHelper;
+ private final SoapMessageHelper soapMessageHelper;
+
+ public SimulatorSoapEndpointPoller(TestContextFactory testContextFactory, SoapMessageHelper soapMessageHelper) {
+ super(testContextFactory);
+
+ this.soapMessageHelper = soapMessageHelper;
+ }
@Override
protected Message processRequestMessage(Message request) {
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 5db90db7c..8f4ee0b40 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
@@ -1,3 +1,19 @@
+/*
+ * 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.swagger.models.ArrayModel;
@@ -43,9 +59,6 @@
import static org.citrusframework.actions.EchoAction.Builder.echo;
-/**
- * @author Christoph Deppisch
- */
public class HttpOperationScenario extends AbstractSimulatorScenario {
/** Operation in wsdl */
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java
index e91e9ed45..bd838b82e 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/http/SimulatorRestAutoConfiguration.java
@@ -16,14 +16,10 @@
package org.citrusframework.simulator.http;
+import jakarta.annotation.Nullable;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
import org.citrusframework.endpoint.EndpointAdapter;
import org.citrusframework.endpoint.adapter.EmptyResponseEndpointAdapter;
import org.citrusframework.http.controller.HttpMessageController;
@@ -32,10 +28,14 @@
import org.citrusframework.http.servlet.RequestCachingServletFilter;
import org.citrusframework.report.MessageListeners;
import org.citrusframework.simulator.SimulatorAutoConfiguration;
+import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.citrusframework.simulator.correlation.CorrelationHandlerRegistry;
import org.citrusframework.simulator.endpoint.SimulatorEndpointAdapter;
import org.citrusframework.simulator.listener.SimulatorMessageListener;
import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
+import org.citrusframework.simulator.service.ScenarioExecutorService;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -55,9 +55,12 @@
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
-/**
- * @author Christoph Deppisch
- */
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
@Configuration
@ConditionalOnWebApplication
@AutoConfigureAfter(SimulatorAutoConfiguration.class)
@@ -65,17 +68,24 @@
@ConditionalOnProperty(prefix = "citrus.simulator.rest", value = "enabled", havingValue = "true", matchIfMissing = true)
public class SimulatorRestAutoConfiguration {
- @Autowired(required = false)
- private SimulatorRestConfigurer configurer;
+ private static final String REST_ENDPOINT_ADAPTER_BEAN_NAME = "simulatorRestEndpointAdapter";
- @Autowired
- private SimulatorRestConfigurationProperties simulatorRestConfiguration;
+ private final ApplicationContext applicationContext;
+ private final SimulatorRestConfigurationProperties simulatorRestConfiguration;
+
+ private @Nullable SimulatorRestConfigurer configurer;
/**
* Target Citrus Http controller
*/
private HttpMessageController restController;
+ public SimulatorRestAutoConfiguration(ApplicationContext applicationContext, SimulatorRestConfigurationProperties simulatorRestConfiguration, @Autowired(required = false) @Nullable SimulatorRestConfigurer configurer) {
+ this.applicationContext = applicationContext;
+ this.simulatorRestConfiguration = simulatorRestConfiguration;
+ this.configurer = configurer;
+ }
+
@Bean
public FilterRegistrationBean requestCachingFilter() {
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean<>(new RequestCachingServletFilter());
@@ -94,13 +104,13 @@ public FilterRegistrationBean requestCachingFilter(
}
@Bean
- public HandlerMapping simulatorRestHandlerMapping(ApplicationContext applicationContext, MessageListeners messageListeners, SimulatorMessageListener simulatorMessageListener) {
+ public HandlerMapping simulatorRestHandlerMapping(MessageListeners messageListeners, @Qualifier(REST_ENDPOINT_ADAPTER_BEAN_NAME) SimulatorEndpointAdapter simulatorRestEndpointAdapter, SimulatorMessageListener simulatorMessageListener) {
SimpleUrlHandlerMapping handlerMapping = new SimpleUrlHandlerMapping();
handlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
handlerMapping.setAlwaysUseFullPath(true);
Map mappings = new HashMap<>();
- HttpMessageController controller = createRestController(applicationContext);
+ HttpMessageController controller = createRestController(simulatorRestEndpointAdapter);
getUrlMappings().forEach(urlMapping -> mappings.put(urlMapping, controller));
handlerMapping.setUrlMap(mappings);
@@ -110,8 +120,8 @@ public HandlerMapping simulatorRestHandlerMapping(ApplicationContext application
}
@Bean
- public HandlerAdapter simulatorRestHandlerAdapter(final ApplicationContext applicationContext, RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
- final RequestMappingHandlerMapping handlerMapping = getRequestMappingHandlerMapping(applicationContext);
+ public HandlerAdapter simulatorRestHandlerAdapter(RequestMappingHandlerAdapter requestMappingHandlerAdapter, @Qualifier(REST_ENDPOINT_ADAPTER_BEAN_NAME) SimulatorEndpointAdapter simulatorRestEndpointAdapter) {
+ final RequestMappingHandlerMapping handlerMapping = getRequestMappingHandlerMapping(simulatorRestEndpointAdapter);
requestMappingHandlerAdapter.getMessageConverters().add(0, new SimulatorHttpMessageConverter());
requestMappingHandlerAdapter.getMessageConverters().add(new DelegatingHttpEntityMessageConverter());
@@ -131,12 +141,12 @@ public ModelAndView handle(HttpServletRequest request, HttpServletResponse respo
};
}
- private RequestMappingHandlerMapping getRequestMappingHandlerMapping(ApplicationContext applicationContext) {
+ private RequestMappingHandlerMapping getRequestMappingHandlerMapping(SimulatorEndpointAdapter simulatorRestEndpointAdapter) {
final RequestMappingHandlerMapping handlerMapping = new RequestMappingHandlerMapping() {
@Override
protected void initHandlerMethods() {
- detectHandlerMethods(createRestController(applicationContext));
+ detectHandlerMethods(createRestController(simulatorRestEndpointAdapter));
super.initHandlerMethods();
}
@@ -152,9 +162,9 @@ protected boolean isHandler(Class> beanType) {
return handlerMapping;
}
- @Bean
- public SimulatorEndpointAdapter simulatorRestEndpointAdapter() {
- return new SimulatorEndpointAdapter();
+ @Bean(name = REST_ENDPOINT_ADAPTER_BEAN_NAME)
+ public SimulatorEndpointAdapter simulatorRestEndpointAdapter(CorrelationHandlerRegistry handlerRegistry, ScenarioExecutorService scenarioExecutorService, SimulatorConfigurationProperties configuration) {
+ return new SimulatorEndpointAdapter(applicationContext, handlerRegistry, scenarioExecutorService, configuration);
}
@Bean
@@ -168,20 +178,15 @@ public EndpointAdapter simulatorRestFallbackEndpointAdapter() {
/**
* Gets the Citrus Http REST controller.
- *
- * @param applicationContext
- * @return
*/
- protected HttpMessageController createRestController(ApplicationContext applicationContext) {
+ protected HttpMessageController createRestController(SimulatorEndpointAdapter simulatorRestEndpointAdapter) {
if (restController == null) {
restController = new HttpMessageController();
- SimulatorEndpointAdapter endpointAdapter = simulatorRestEndpointAdapter();
- endpointAdapter.setApplicationContext(applicationContext);
- endpointAdapter.setMappingKeyExtractor(simulatorRestScenarioMapper());
- endpointAdapter.setFallbackEndpointAdapter(simulatorRestFallbackEndpointAdapter());
+ simulatorRestEndpointAdapter.setMappingKeyExtractor(simulatorRestScenarioMapper());
+ simulatorRestEndpointAdapter.setFallbackEndpointAdapter(simulatorRestFallbackEndpointAdapter());
- restController.setEndpointAdapter(endpointAdapter);
+ restController.setEndpointAdapter(simulatorRestEndpointAdapter);
}
return restController;
@@ -198,7 +203,7 @@ public ScenarioMapper simulatorRestScenarioMapper() {
@Bean
protected HandlerInterceptor httpInterceptor(MessageListeners messageListeners,
- SimulatorMessageListener simulatorMessageListener) {
+ SimulatorMessageListener simulatorMessageListener) {
messageListeners.addMessageListener(simulatorMessageListener);
return new InterceptorHttp(messageListeners);
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/jms/SimulatorJmsAutoConfiguration.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/jms/SimulatorJmsAutoConfiguration.java
index ef8488930..a4372c48c 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/jms/SimulatorJmsAutoConfiguration.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/jms/SimulatorJmsAutoConfiguration.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -16,7 +16,9 @@
package org.citrusframework.simulator.jms;
+import jakarta.annotation.Nullable;
import jakarta.jms.ConnectionFactory;
+import org.citrusframework.context.TestContextFactory;
import org.citrusframework.endpoint.EndpointAdapter;
import org.citrusframework.endpoint.adapter.EmptyResponseEndpointAdapter;
import org.citrusframework.jms.endpoint.JmsEndpoint;
@@ -25,12 +27,16 @@
import org.citrusframework.jms.endpoint.JmsSyncEndpointConfiguration;
import org.citrusframework.simulator.SimulatorAutoConfiguration;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.citrusframework.simulator.correlation.CorrelationHandlerRegistry;
import org.citrusframework.simulator.endpoint.SimulatorEndpointAdapter;
import org.citrusframework.simulator.endpoint.SimulatorEndpointPoller;
import org.citrusframework.simulator.endpoint.SimulatorSoapEndpointPoller;
import org.citrusframework.simulator.scenario.mapper.ContentBasedXPathScenarioMapper;
import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
+import org.citrusframework.simulator.service.ScenarioExecutorService;
+import org.citrusframework.simulator.ws.SoapMessageHelper;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -47,14 +53,18 @@
@ConditionalOnProperty(prefix = "citrus.simulator.jms", value = "enabled", havingValue = "true")
public class SimulatorJmsAutoConfiguration {
- @Autowired(required = false)
- private SimulatorJmsConfigurer configurer;
+ private static final String JMS_ENDPOINT_ADAPTER_BEAN_NAME = "simulatorJmsEndpointAdapter";
- @Autowired
- private SimulatorJmsConfigurationProperties simulatorJmsConfiguration;
+ private final SimulatorConfigurationProperties simulatorConfiguration;
+ private final SimulatorJmsConfigurationProperties simulatorJmsConfiguration;
- @Autowired
- private SimulatorConfigurationProperties simulatorConfiguration;
+ private @Nullable SimulatorJmsConfigurer configurer;
+
+ public SimulatorJmsAutoConfiguration(SimulatorConfigurationProperties simulatorConfiguration, SimulatorJmsConfigurationProperties simulatorJmsConfiguration, @Autowired(required = false) @Nullable SimulatorJmsConfigurer configurer) {
+ this.simulatorConfiguration = simulatorConfiguration;
+ this.simulatorJmsConfiguration = simulatorJmsConfiguration;
+ this.configurer = configurer;
+ }
@Bean
@ConditionalOnMissingBean
@@ -91,9 +101,9 @@ protected JmsEndpoint simulatorJmsInboundEndpoint(ConnectionFactory connectionFa
}
}
- @Bean
- public SimulatorEndpointAdapter simulatorJmsEndpointAdapter() {
- return new SimulatorEndpointAdapter();
+ @Bean(JMS_ENDPOINT_ADAPTER_BEAN_NAME)
+ public SimulatorEndpointAdapter simulatorJmsEndpointAdapter(ApplicationContext applicationContext, CorrelationHandlerRegistry handlerRegistry, ScenarioExecutorService scenarioExecutorService, SimulatorConfigurationProperties configuration) {
+ return new SimulatorEndpointAdapter(applicationContext, handlerRegistry, scenarioExecutorService, configuration);
}
@Bean
@@ -106,30 +116,27 @@ public ScenarioMapper simulatorJmsScenarioMapper() {
}
@Bean
- public SimulatorEndpointPoller simulatorJmsEndpointPoller(ApplicationContext applicationContext,
- ConnectionFactory connectionFactory) {
+ public SimulatorEndpointPoller simulatorJmsEndpointPoller(ConnectionFactory connectionFactory, @Qualifier(JMS_ENDPOINT_ADAPTER_BEAN_NAME) SimulatorEndpointAdapter simulatorJmsEndpointAdapter, SoapMessageHelper soapMessageHelper, TestContextFactory testContextFactory) {
SimulatorEndpointPoller endpointPoller;
if (useSoap()) {
- endpointPoller = new SimulatorSoapEndpointPoller();
+ endpointPoller = new SimulatorSoapEndpointPoller(testContextFactory, soapMessageHelper);
} else {
- endpointPoller = new SimulatorEndpointPoller();
+ endpointPoller = new SimulatorEndpointPoller(testContextFactory);
}
endpointPoller.setInboundEndpoint(simulatorJmsInboundEndpoint(connectionFactory));
- SimulatorEndpointAdapter endpointAdapter = simulatorJmsEndpointAdapter();
- endpointAdapter.setApplicationContext(applicationContext);
- endpointAdapter.setMappingKeyExtractor(simulatorJmsScenarioMapper());
- endpointAdapter.setFallbackEndpointAdapter(simulatorJmsFallbackEndpointAdapter());
+ simulatorJmsEndpointAdapter.setMappingKeyExtractor(simulatorJmsScenarioMapper());
+ simulatorJmsEndpointAdapter.setFallbackEndpointAdapter(simulatorJmsFallbackEndpointAdapter());
if (!isSynchronous()) {
- endpointAdapter.setHandleResponse(false);
+ simulatorJmsEndpointAdapter.setHandleResponse(false);
}
endpointPoller.setExceptionDelay(exceptionDelay(simulatorConfiguration));
- endpointPoller.setEndpointAdapter(endpointAdapter);
+ endpointPoller.setEndpointAdapter(simulatorJmsEndpointAdapter);
return endpointPoller;
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java
index 8f31a578c..812afacaf 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/listener/SimulatorStatusListener.java
@@ -41,9 +41,6 @@
import static org.citrusframework.util.StringUtils.hasText;
import static org.springframework.util.StringUtils.arrayToCommaDelimitedString;
-/**
- * @author Christoph Deppisch
- */
@Component
public class SimulatorStatusListener extends AbstractTestListener implements TestActionListener {
@@ -92,7 +89,7 @@ public void onTestSuccess(TestCase testCase) {
scenarioExecutionService.completeScenarioExecution(getScenarioExecutionId(testCase), new org.citrusframework.simulator.model.TestResult(testResult));
- logger.info("Test succeeded: {}", testResult);
+ logger.info("Scenario succeeded: {}", testResult);
}
@Override
@@ -106,7 +103,7 @@ public void onTestFailure(TestCase testCase, Throwable cause) {
scenarioExecutionService.completeScenarioExecution(getScenarioExecutionId(testCase), new org.citrusframework.simulator.model.TestResult(testResult));
- logger.info("Test failed: {}", testResult);
+ logger.info("Scenario failed: {}", testResult);
}
@Override
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenario.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenario.java
index 5b93117fa..a9e5290aa 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenario.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenario.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -16,36 +16,42 @@
package org.citrusframework.simulator.scenario;
+import jakarta.annotation.Nullable;
import jakarta.annotation.PostConstruct;
+import org.citrusframework.TestCaseRunner;
import org.citrusframework.simulator.correlation.CorrelationHandler;
import org.citrusframework.simulator.correlation.CorrelationHandlerBuilder;
-/**
- * @author Christoph Deppisch
- */
-public abstract class AbstractSimulatorScenario implements SimulatorScenario, CorrelationHandler {
+public abstract class AbstractSimulatorScenario implements CorrelationHandler, SimulatorScenario {
- /**
- * Scenario endpoint
- */
private ScenarioEndpoint scenarioEndpoint;
+ private @Nullable TestCaseRunner testCaseRunner;
+
@PostConstruct
public void init() {
scenarioEndpoint = new ScenarioEndpoint(new ScenarioEndpointConfiguration());
}
+ @Override
+ public ScenarioEndpoint getScenarioEndpoint() {
+ return scenarioEndpoint;
+ }
+
+ @Override
+ public @Nullable TestCaseRunner getTestCaseRunner() {
+ return testCaseRunner;
+ }
+
+ @Override
+ public void setTestCaseRunner(TestCaseRunner testCaseRunner) {
+ this.testCaseRunner = testCaseRunner;
+ }
+
/**
* Start new message correlation so scenario is provided with additional inbound messages.
- *
- * @return
*/
public CorrelationHandlerBuilder correlation() {
return new CorrelationHandlerBuilder(scenarioEndpoint);
}
-
- @Override
- public ScenarioEndpoint getScenarioEndpoint() {
- return scenarioEndpoint;
- }
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioEndpoint.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioEndpoint.java
index ce83adfa5..d0abfc01f 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioEndpoint.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioEndpoint.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -22,16 +22,17 @@
import org.citrusframework.messaging.Consumer;
import org.citrusframework.messaging.Producer;
import org.citrusframework.simulator.endpoint.EndpointMessageHandler;
+import org.citrusframework.simulator.endpoint.SimulationFailedUnexpectedlyException;
import org.citrusframework.simulator.exception.SimulatorException;
import java.util.Stack;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-/**
- * @author Christoph Deppisch
- */
+import static java.lang.Thread.currentThread;
+import static java.util.Objects.isNull;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+
public class ScenarioEndpoint extends AbstractEndpoint implements Producer, Consumer {
/**
@@ -81,14 +82,17 @@ public Message receive(TestContext context) {
@Override
public Message receive(TestContext context, long timeout) {
try {
- Message message = channel.poll(timeout, TimeUnit.MILLISECONDS);
- if (message == null) {
+ Message message = channel.poll(timeout, MILLISECONDS);
+
+ if (isNull(message)) {
throw new SimulatorException("Failed to receive scenario inbound message");
}
+
messageReceived(message, context);
+
return message;
} catch (InterruptedException e) {
- Thread.currentThread().interrupt();
+ currentThread().interrupt();
throw new SimulatorException(e);
}
}
@@ -96,8 +100,16 @@ public Message receive(TestContext context, long timeout) {
@Override
public void send(Message message, TestContext context) {
messageSent(message, context);
+ completeNextResponseFuture(message);
+ }
+
+ void fail(Throwable e) {
+ completeNextResponseFuture(new SimulationFailedUnexpectedlyException(e));
+ }
+
+ private void completeNextResponseFuture(Message message) {
if (responseFutures.isEmpty()) {
- throw new SimulatorException("Failed to process scenario response message - missing response consumer");
+ throw new SimulatorException("Failed to process scenario response message - missing response consumer!");
} else {
responseFutures.pop().complete(message);
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioRunner.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioRunner.java
index 275bb63b4..b51f5efd6 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioRunner.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/ScenarioRunner.java
@@ -1,5 +1,5 @@
/*
- * Copyright 2006-2017 the original author or authors.
+ * Copyright 2006-2024 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.
@@ -16,6 +16,7 @@
package org.citrusframework.simulator.scenario;
+import lombok.Getter;
import org.citrusframework.DefaultTestCaseRunner;
import org.citrusframework.GherkinTestActionRunner;
import org.citrusframework.TestAction;
@@ -39,21 +40,12 @@ public class ScenarioRunner implements GherkinTestActionRunner {
private final TestCaseRunner delegate;
- /**
- * Scenario direct endpoint
- */
+ @Getter
private final ScenarioEndpoint scenarioEndpoint;
- /** Spring bean application context */
+ @Getter
private final ApplicationContext applicationContext;
- /**
- * Default constructor using fields.
- *
- * @param scenarioEndpoint
- * @param applicationContext
- * @param context
- */
public ScenarioRunner(ScenarioEndpoint scenarioEndpoint, ApplicationContext applicationContext, TestContext context) {
this.scenarioEndpoint = scenarioEndpoint;
this.applicationContext = applicationContext;
@@ -61,15 +53,6 @@ public ScenarioRunner(ScenarioEndpoint scenarioEndpoint, ApplicationContext appl
this.delegate = new DefaultTestCaseRunner(context);
}
- /**
- * Gets the scenario inbound endpoint.
- *
- * @return
- */
- public ScenarioEndpoint scenarioEndpoint() {
- return scenarioEndpoint;
- }
-
public SendMessageAction.Builder send() {
return SendMessageAction.Builder.send().endpoint(scenarioEndpoint);
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/SimulatorScenario.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/SimulatorScenario.java
index 710272dcc..709b04743 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/SimulatorScenario.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/scenario/SimulatorScenario.java
@@ -16,14 +16,16 @@
package org.citrusframework.simulator.scenario;
+import jakarta.annotation.Nullable;
+import org.citrusframework.DefaultTestCaseRunner;
+import org.citrusframework.TestCaseRunner;
import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.simulator.exception.SimulatorException;
import static java.lang.String.format;
+import static java.util.Objects.nonNull;
import static org.citrusframework.simulator.scenario.ScenarioUtils.getAnnotationFromClassHierarchy;
-/**
- * @author Christoph Deppisch
- */
public interface SimulatorScenario {
/**
@@ -41,21 +43,36 @@ default String getName() {
return getNameFromScenarioAnnotation();
}
+ @Nullable
+ TestCaseRunner getTestCaseRunner();
+
+ void setTestCaseRunner(TestCaseRunner testCaseRunner);
+
+ default Void registerException(Throwable e) {
+ if (nonNull(getTestCaseRunner()) && getTestCaseRunner() instanceof DefaultTestCaseRunner defaultTestCaseRunner) {
+ defaultTestCaseRunner.getContext().addException(new CitrusRuntimeException(e));
+ }
+
+ getScenarioEndpoint().fail(e);
+
+ return null;
+ }
+
/**
* Retrieves the name of a scenario from its {@link Scenario} annotation.
*
* @return the name of the scenario as specified by the {@link Scenario} annotation's value.
- * @throws CitrusRuntimeException if the {@link Scenario} annotation is not found on this
- * scenario, its proxied objects or superclasses.
+ * @throws SimulatorException if the {@link Scenario} annotation is not found on this scenario, its proxied objects or superclasses.
*/
private String getNameFromScenarioAnnotation() {
Scenario scenarioAnnotation = getAnnotationFromClassHierarchy(this, Scenario.class);
if (scenarioAnnotation == null) {
- throw new CitrusRuntimeException(
- format("Missing scenario annotation at class: %s - even searched class hierarchy", getClass())
+ throw new SimulatorException(
+ format("Missing scenario annotation at class: %s - even searched class hierarchy!", getClass())
);
}
+
return scenarioAnnotation.value();
}
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorService.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorService.java
index 402cdc60d..9f7ba4512 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorService.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorService.java
@@ -34,6 +34,7 @@
import java.util.List;
import java.util.concurrent.ExecutorService;
+import static java.util.concurrent.CompletableFuture.runAsync;
import static java.util.concurrent.Executors.newFixedThreadPool;
/**
@@ -122,7 +123,8 @@ public void startScenario(Long executionId, String name, SimulatorScenario scena
* @param scenarioParameters the list of parameters to pass to the scenario when starting
*/
private void startScenarioAsync(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) {
- executorService.submit(() -> super.startScenario(executionId, name, scenario, scenarioParameters));
+ runAsync(() -> super.startScenario(executionId, name, scenario, scenarioParameters), executorService)
+ .exceptionally(scenario::registerException);
}
private void shutdownExecutor() {
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorService.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorService.java
index 2e161aec4..1bfcd8d34 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorService.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorService.java
@@ -19,6 +19,7 @@
import jakarta.annotation.Nullable;
import org.citrusframework.Citrus;
import org.citrusframework.context.TestContext;
+import org.citrusframework.exceptions.TestCaseFailedException;
import org.citrusframework.simulator.model.ScenarioExecution;
import org.citrusframework.simulator.model.ScenarioParameter;
import org.citrusframework.simulator.scenario.ScenarioRunner;
@@ -33,6 +34,7 @@
import java.util.List;
+import static java.lang.String.format;
import static org.citrusframework.annotations.CitrusAnnotations.injectAll;
import static org.citrusframework.simulator.model.ScenarioExecution.EXECUTION_ID;
@@ -109,13 +111,11 @@ public final Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters) {
logger.info("Starting scenario : {}", name);
- try {
- var context = createTestContext();
- createAndRunScenarioRunner(context, executionId, name, scenario, scenarioParameters);
- logger.debug("Scenario completed: {}", name);
- } catch (Exception e) {
- logger.error("Scenario completed with error: {}!", name, e);
- }
+
+ var context = createTestContext();
+ createAndRunScenarioRunner(context, executionId, name, scenario, scenarioParameters);
+
+ logger.debug("Scenario completed: {}", name);
}
/**
@@ -137,13 +137,20 @@ private void createAndRunScenarioRunner(TestContext context, Long executionId, S
}
runner.variable(EXECUTION_ID, executionId);
- runner.name(String.format("Scenario(%s)", name));
+ runner.name(format("Scenario(%s)", name));
- injectAll(scenario, citrus, context);
+ injectAll(scenario, citrus);
try {
runner.start();
+ scenario.setTestCaseRunner(runner.getTestCaseRunner());
scenario.run(runner);
+ } catch (TestCaseFailedException e) {
+ logger.error("Registered forced failure of scenario: {}!", name, e);
+ } catch (Exception e) {
+ logger.error("Scenario completed with error: {}!", name, e);
+ scenario.registerException(e);
+ throw e;
} finally {
runner.stop();
}
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/SimulatorWebServiceAutoConfiguration.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/SimulatorWebServiceAutoConfiguration.java
index f6fd993cf..5e3d392b1 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/SimulatorWebServiceAutoConfiguration.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/SimulatorWebServiceAutoConfiguration.java
@@ -16,18 +16,19 @@
package org.citrusframework.simulator.ws;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
import org.citrusframework.endpoint.EndpointAdapter;
import org.citrusframework.endpoint.adapter.EmptyResponseEndpointAdapter;
import org.citrusframework.simulator.SimulatorAutoConfiguration;
+import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.citrusframework.simulator.correlation.CorrelationHandlerRegistry;
import org.citrusframework.simulator.endpoint.SimulatorEndpointAdapter;
import org.citrusframework.simulator.scenario.mapper.ContentBasedXPathScenarioMapper;
import org.citrusframework.simulator.scenario.mapper.ScenarioMapper;
+import org.citrusframework.simulator.service.ScenarioExecutorService;
import org.citrusframework.ws.interceptor.LoggingEndpointInterceptor;
import org.citrusframework.ws.server.WebServiceEndpoint;
import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
@@ -47,9 +48,10 @@
import org.springframework.ws.server.endpoint.mapping.UriEndpointMapping;
import org.springframework.ws.transport.http.MessageDispatcherServlet;
-/**
- * @author Christoph Deppisch
- */
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
@Configuration
@ConditionalOnWebApplication
@AutoConfigureAfter(SimulatorAutoConfiguration.class)
@@ -58,6 +60,8 @@
@ConditionalOnProperty(prefix = "citrus.simulator.ws", value = "enabled", havingValue = "true")
public class SimulatorWebServiceAutoConfiguration {
+ private static final String WS_ENDPOINT_ADAPTER_BEAN_NAME = "simulatorWsEndpointAdapter";
+
@Autowired(required = false)
private SimulatorWebServiceConfigurer configurer;
@@ -81,32 +85,30 @@ public ServletRegistrationBean simulatorServletRegistr
}
@Bean
- public EndpointMapping simulatorWsEndpointMapping(ApplicationContext applicationContext) {
+ public EndpointMapping simulatorWsEndpointMapping(MessageEndpoint simulatorWsEndpoint) {
UriEndpointMapping endpointMapping = new UriEndpointMapping();
endpointMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
- endpointMapping.setDefaultEndpoint(simulatorWsEndpoint(applicationContext));
+ endpointMapping.setDefaultEndpoint(simulatorWsEndpoint);
endpointMapping.setInterceptors(interceptors());
return endpointMapping;
}
@Bean
- public MessageEndpoint simulatorWsEndpoint(ApplicationContext applicationContext) {
+ public MessageEndpoint simulatorWsEndpoint(@Qualifier(WS_ENDPOINT_ADAPTER_BEAN_NAME) SimulatorEndpointAdapter simulatorWsEndpointAdapter) {
WebServiceEndpoint webServiceEndpoint = new WebServiceEndpoint();
- SimulatorEndpointAdapter endpointAdapter = simulatorWsEndpointAdapter();
- endpointAdapter.setApplicationContext(applicationContext);
- endpointAdapter.setMappingKeyExtractor(simulatorWsScenarioMapper());
- endpointAdapter.setFallbackEndpointAdapter(simulatorWsFallbackEndpointAdapter());
+ simulatorWsEndpointAdapter.setMappingKeyExtractor(simulatorWsScenarioMapper());
+ simulatorWsEndpointAdapter.setFallbackEndpointAdapter(simulatorWsFallbackEndpointAdapter());
- webServiceEndpoint.setEndpointAdapter(endpointAdapter);
+ webServiceEndpoint.setEndpointAdapter(simulatorWsEndpointAdapter);
return webServiceEndpoint;
}
- @Bean
- public SimulatorEndpointAdapter simulatorWsEndpointAdapter() {
- return new SimulatorEndpointAdapter();
+ @Bean(name = WS_ENDPOINT_ADAPTER_BEAN_NAME)
+ public SimulatorEndpointAdapter simulatorWsEndpointAdapter(ApplicationContext applicationContext, CorrelationHandlerRegistry handlerRegistry, ScenarioExecutorService scenarioExecutorService, SimulatorConfigurationProperties configuration) {
+ return new SimulatorEndpointAdapter(applicationContext, handlerRegistry, scenarioExecutorService, configuration);
}
@Bean
@@ -134,7 +136,6 @@ public EndpointAdapter simulatorWsFallbackEndpointAdapter() {
* (enabled).
*
* @return a default web service configuration support
- *
* @see Combined Sample of REST and WS
*/
@Bean
diff --git a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/WsdlOperationScenario.java b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/WsdlOperationScenario.java
index eb0cdb472..8a5bae859 100644
--- a/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/WsdlOperationScenario.java
+++ b/simulator-spring-boot/src/main/java/org/citrusframework/simulator/ws/WsdlOperationScenario.java
@@ -1,3 +1,19 @@
+/*
+ * Copyright 2024 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.ws;
import org.citrusframework.message.MessageHeaders;
@@ -10,9 +26,6 @@
import static org.citrusframework.actions.EchoAction.Builder.echo;
-/**
- * @author Christoph Deppisch
- */
public class WsdlOperationScenario extends AbstractSimulatorScenario {
/** Operation in wsdl */
diff --git a/simulator-spring-boot/src/main/resources/META-INF/citrus-simulator.properties b/simulator-spring-boot/src/main/resources/META-INF/citrus-simulator.properties
index ed7dfbb3e..ab90c6a73 100644
--- a/simulator-spring-boot/src/main/resources/META-INF/citrus-simulator.properties
+++ b/simulator-spring-boot/src/main/resources/META-INF/citrus-simulator.properties
@@ -41,6 +41,7 @@ info.simulator.version=@project.version@
# Logging
logging.level.org.citrusframework.simulator=INFO
+logging.level.org.citrusframework.report.LoggingReporter=WARN
# Do not automatically create transaction contexts
spring.jpa.open-in-view=false
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/AsynchronousSimulatorEndpointAdapterIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/AsynchronousSimulatorEndpointAdapterIT.java
new file mode 100644
index 000000000..4bc0c0fe8
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/AsynchronousSimulatorEndpointAdapterIT.java
@@ -0,0 +1,26 @@
+package org.citrusframework.simulator.endpoint;
+
+import org.citrusframework.simulator.exception.SimulatorException;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
+import org.springframework.test.annotation.DirtiesContext;
+import org.springframework.test.context.TestPropertySource;
+import org.springframework.web.server.ResponseStatusException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.throwable;
+
+@Isolated
+@DirtiesContext
+@TestPropertySource(properties = {"citrus.simulator.mode=async"})
+class AsynchronousSimulatorEndpointAdapterIT extends SimulatorEndpointAdapterIT {
+
+ @Test
+ void dispatchMessage_returnsExceptionMessage_ifUnderlyingScenarioExecutionFails() {
+ verifyFailingScenarioThrowsResponseStatusException(
+ e -> assertThat(e).extracting(ResponseStatusException::getCause)
+ .asInstanceOf(throwable(SimulatorException.class))
+ .hasMessage(FAIL_WITH_PURPOSE)
+ );
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyExceptionTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyExceptionTest.java
new file mode 100644
index 000000000..fe36cd746
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyExceptionTest.java
@@ -0,0 +1,27 @@
+package org.citrusframework.simulator.endpoint;
+
+import org.citrusframework.simulator.exception.SimulatorException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.citrusframework.simulator.endpoint.SimulationFailedUnexpectedlyException.EXCEPTION_TYPE;
+
+class SimulationFailedUnexpectedlyExceptionTest {
+
+ private static final Throwable TEST_THROWABLE = new SimulatorException("Huston, we hav a problem!");
+
+ private SimulationFailedUnexpectedlyException fixture;
+
+ @BeforeEach
+ void beforeEachSetup() {
+ fixture = new SimulationFailedUnexpectedlyException(TEST_THROWABLE);
+ }
+
+ @Test
+ void typeIsStatic() {
+ assertThat(fixture)
+ .extracting(SimulationFailedUnexpectedlyException::getType)
+ .isEqualTo(EXCEPTION_TYPE);
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapterIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapterIT.java
new file mode 100644
index 000000000..4c6361a39
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapterIT.java
@@ -0,0 +1,94 @@
+package org.citrusframework.simulator.endpoint;
+
+import org.assertj.core.api.ThrowingConsumer;
+import org.citrusframework.context.TestContextFactory;
+import org.citrusframework.message.DefaultMessage;
+import org.citrusframework.message.Message;
+import org.citrusframework.simulator.IntegrationTest;
+import org.citrusframework.simulator.exception.SimulatorException;
+import org.citrusframework.simulator.scenario.AbstractSimulatorScenario;
+import org.citrusframework.simulator.scenario.Scenario;
+import org.citrusframework.simulator.scenario.ScenarioRunner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.HttpStatusCode;
+import org.springframework.web.server.ResponseStatusException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.assertj.core.api.InstanceOfAssertFactories.throwable;
+
+@IntegrationTest
+@ExtendWith({MockitoExtension.class})
+abstract class SimulatorEndpointAdapterIT {
+
+ protected static final String FAIL_WITH_PURPOSE = "Fail with purpose!";
+
+ private static final String NO_RESPONSE_SCENARIO_NAME = "SimulatorEndpointAdapterIT:no-response-scenario";
+ private static final String SUCCESS_SCENARIO_NAME = "SimulatorEndpointAdapterIT:success-scenario";
+ private static final String FAIL_SCENARIO_NAME = "SimulatorEndpointAdapterIT:fail-scenario";
+
+ @Mock
+ private Message messageMock;
+
+ @Autowired
+ private SimulatorEndpointAdapter fixture;
+
+ @Test
+ void dispatchMessage_returnsNull_withoutResponse() {
+ var result = fixture.dispatchMessage(messageMock, NO_RESPONSE_SCENARIO_NAME);
+ assertThat(result)
+ .isNull();
+ }
+
+ @Test
+ void dispatchMessage_returnsResponse() {
+ var result = fixture.dispatchMessage(messageMock, SUCCESS_SCENARIO_NAME);
+ assertThat(result)
+ .isInstanceOf(DefaultMessage.class);
+ }
+
+ void verifyFailingScenarioThrowsResponseStatusException(ThrowingConsumer exceptionAssert) {
+ assertThatThrownBy(() -> fixture.dispatchMessage(messageMock, FAIL_SCENARIO_NAME))
+ .asInstanceOf(throwable(ResponseStatusException.class))
+ .satisfies(
+ e -> assertThat(e).extracting(ResponseStatusException::getStatusCode)
+ .isEqualTo(HttpStatusCode.valueOf(555)),
+ e -> assertThat(e).extracting(ResponseStatusException::getReason)
+ .isEqualTo("Simulation failed with an Exception!"),
+ exceptionAssert
+ );
+ }
+
+ @Scenario(NO_RESPONSE_SCENARIO_NAME)
+ private static class NoResponseScenario extends AbstractSimulatorScenario {
+ }
+
+ @Scenario(SUCCESS_SCENARIO_NAME)
+ private static class SuccessScenario extends AbstractSimulatorScenario {
+
+ private final TestContextFactory testContextFactory;
+
+ private SuccessScenario(TestContextFactory testContextFactory) {
+ this.testContextFactory = testContextFactory;
+ }
+
+ @Override
+ public void run(ScenarioRunner runner) {
+ var context = testContextFactory.getObject();
+ getScenarioEndpoint().send(new DefaultMessage(), context);
+ }
+ }
+
+ @Scenario(FAIL_SCENARIO_NAME)
+ private static class FailScenario extends AbstractSimulatorScenario {
+
+ @Override
+ public void run(ScenarioRunner runner) {
+ throw new SimulatorException(FAIL_WITH_PURPOSE);
+ }
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SynchronousSimulatorEndpointAdapterIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SynchronousSimulatorEndpointAdapterIT.java
new file mode 100644
index 000000000..14fe0f50d
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SynchronousSimulatorEndpointAdapterIT.java
@@ -0,0 +1,23 @@
+package org.citrusframework.simulator.endpoint;
+
+import org.citrusframework.exceptions.TestCaseFailedException;
+import org.citrusframework.simulator.exception.SimulatorException;
+import org.junit.jupiter.api.Test;
+import org.springframework.web.server.ResponseStatusException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.InstanceOfAssertFactories.throwable;
+
+class SynchronousSimulatorEndpointAdapterIT extends SimulatorEndpointAdapterIT {
+
+ @Test
+ void dispatchMessage_returnsExceptionMessage_ifUnderlyingScenarioExecutionFails() {
+ verifyFailingScenarioThrowsResponseStatusException(
+ e -> assertThat(e).extracting(ResponseStatusException::getCause)
+ .asInstanceOf(throwable(TestCaseFailedException.class))
+ .rootCause()
+ .asInstanceOf(throwable(SimulatorException.class))
+ .hasMessage(FAIL_WITH_PURPOSE)
+ );
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventIT.java
index 8dbf6e941..bf950b9d9 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventIT.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventIT.java
@@ -16,9 +16,8 @@
package org.citrusframework.simulator.events;
-import org.apache.commons.lang3.NotImplementedException;
import org.citrusframework.simulator.IntegrationTest;
-import org.citrusframework.simulator.scenario.ScenarioEndpoint;
+import org.citrusframework.simulator.scenario.AbstractSimulatorTestScenario;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.citrusframework.simulator.service.ScenarioLookupService;
import org.citrusframework.simulator.web.rest.ScenarioResource;
@@ -70,11 +69,6 @@ private int countScenarioResourceScenarios() {
.size();
}
- private static final class ScenariosReloadedEventITScenario implements SimulatorScenario {
-
- @Override
- public ScenarioEndpoint getScenarioEndpoint() {
- throw new NotImplementedException();
- }
+ private static final class ScenariosReloadedEventITScenario extends AbstractSimulatorTestScenario {
}
}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationMatcherTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationMatcherTest.java
index 4e6c457ac..4a81e2018 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationMatcherTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationMatcherTest.java
@@ -20,6 +20,7 @@
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.params.provider.Arguments.arguments;
import static org.mockito.Mockito.doReturn;
class HttpRequestAnnotationMatcherTest {
@@ -36,170 +37,170 @@ class HttpRequestAnnotationMatcherTest {
static Stream checkRequestPathSupported() {
return Stream.of(
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_NAME,
setupHttpMessage("/path/name", RequestMethod.GET, Collections.emptyMap()),
true,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_NAME,
setupHttpMessage("/path/name", RequestMethod.GET, Collections.emptyMap()),
false,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_NAME,
setupHttpMessage("/path/wrong-path", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_NAME,
setupHttpMessage("", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_VALUE,
setupHttpMessage("/path/value", RequestMethod.GET, Collections.emptyMap()),
true,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_VALUE,
setupHttpMessage("/path/wrong-path", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_VALUE,
setupHttpMessage("", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PLACEHOLDER,
setupHttpMessage("/path/place-holder/123", RequestMethod.GET, Collections.emptyMap()),
false,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PLACEHOLDER,
setupHttpMessage("/path/place-holder/123", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PLACEHOLDER,
setupHttpMessage("/path/wrong-path", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PLACEHOLDER,
setupHttpMessage("/path/wrong-path", RequestMethod.GET, Collections.emptyMap()),
false,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PATTERN,
setupHttpMessage("/path/pattern/match-me", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PATTERN,
setupHttpMessage("/path/pattern/match-me", RequestMethod.GET, Collections.emptyMap()),
false,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PATTERN,
setupHttpMessage("/path/wrong-pattern", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PATTERN,
setupHttpMessage("/path/wrong-pattern", RequestMethod.GET, Collections.emptyMap()),
false,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PATTERN,
setupHttpMessage("", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PATH_PATTERN,
setupHttpMessage("", RequestMethod.GET, Collections.emptyMap()),
false,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PUT_METHOD,
setupHttpMessage("/any-path", RequestMethod.PUT, Collections.emptyMap()),
true,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_PUT_METHOD,
setupHttpMessage("", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_QUERY_PARAMS,
setupHttpMessage("/any-path", RequestMethod.GET, Collections.singletonMap("a", Collections.singleton("1"))),
true,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_QUERY_PARAMS,
setupHttpMessage("/any-path", RequestMethod.GET, Collections.singletonMap("a", Collections.emptySet())),
true,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_QUERY_PARAMS,
setupHttpMessage("/any-path", RequestMethod.GET, Stream.of("a=1", "b=2").map(item -> item.split("=")).collect(Collectors.toMap(keyValuePair -> keyValuePair[0], keyValuePair -> Collections.singleton(keyValuePair[1])))),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_QUERY_PARAMS,
setupHttpMessage("/any-path", RequestMethod.GET, Collections.singletonMap("c", Collections.singleton("3"))),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_QUERY_PARAMS,
setupHttpMessage("/any-path", RequestMethod.GET, Collections.emptyMap()),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_ALL_SUPPORTED_RESTRICTIONS,
setupHttpMessage("/path/value", RequestMethod.GET, Collections.singletonMap("a", Collections.singleton("1"))),
true,
true
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_ALL_SUPPORTED_RESTRICTIONS,
setupHttpMessage("/wrong-path", RequestMethod.GET, Collections.singletonMap("a", Collections.singleton("1"))),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_ALL_SUPPORTED_RESTRICTIONS,
setupHttpMessage("/path/value", RequestMethod.PUT, Collections.singletonMap("a", Collections.singleton("1"))),
true,
false
),
- Arguments.of(
+ arguments(
REQ_MAP_WITH_ALL_SUPPORTED_RESTRICTIONS,
setupHttpMessage("/path/value", RequestMethod.GET, Collections.emptyMap()),
true,
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperIT.java
index ce091cba3..09f59d159 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperIT.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperIT.java
@@ -1,19 +1,3 @@
-/*
- * Copyright 2024 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.aspectj.lang.ProceedingJoinPoint;
@@ -45,9 +29,6 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.springframework.http.HttpMethod.PUT;
-/**
- * @author Thorsten Schlathoelter
- */
@ExtendWith(SpringExtension.class)
@Import(AspectTestConfiguration.class)
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperTest.java
index 4674fa96f..a94e6c84c 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/HttpRequestAnnotationScenarioMapperTest.java
@@ -1,19 +1,3 @@
-/*
- * Copyright 2024 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 jakarta.annotation.Nonnull;
@@ -31,7 +15,7 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
-import java.util.Arrays;
+import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
@@ -41,9 +25,6 @@
import static org.springframework.http.HttpMethod.POST;
import static org.springframework.http.HttpMethod.PUT;
-/**
- * @author Christoph Deppisch
- */
@ExtendWith(MockitoExtension.class)
class HttpRequestAnnotationScenarioMapperTest {
@@ -62,16 +43,18 @@ void beforeEachSetup() {
@Test
void testGetMappingKey() {
- fixture.setScenarioList(Arrays.asList(new IssueScenario(),
- new FooScenario(),
- new SubclassedFooScenario(),
- new GetFooScenario(),
- new PutFooScenario(),
- new OtherScenario()));
+ fixture.setScenarioList(
+ List.of(
+ new IssueScenario(),
+ new FooScenario(),
+ new SubclassedFooScenario(),
+ new GetFooScenario(),
+ new PutFooScenario(),
+ new OtherScenario()));
assertEquals("FooScenario", mappingKeyFor(fixture, "/issues/foo"));
assertEquals("GetFooScenario", mappingKeyFor(fixture, "/issues/foo", GET));
- assertEquals("PutFooScenario", mappingKeyFor(fixture, "/issues/foo",PUT));
+ assertEquals("PutFooScenario", mappingKeyFor(fixture, "/issues/foo", PUT));
assertEquals("FooScenario", mappingKeyFor(fixture, "/issues/foo/sub", POST));
assertEquals("OtherScenario", mappingKeyFor(fixture, "/issues/other"));
assertEquals("IssueScenario", mappingKeyFor(fixture, "/issues/bar", GET));
@@ -92,7 +75,7 @@ private String mappingKeyFor(HttpRequestAnnotationScenarioMapper mapper, String
return mapper.getMappingKey(new HttpMessage().path(path));
}
- private String mappingKeyFor(HttpRequestAnnotationScenarioMapper mapper, String path, @Nonnull HttpMethod method) {
+ private String mappingKeyFor(HttpRequestAnnotationScenarioMapper mapper, String path, @Nonnull HttpMethod method) {
return mapper.getMappingKey(new HttpMessage().path(path).method(method));
}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/SimulatorRestAutoConfigurationTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/SimulatorRestAutoConfigurationTest.java
index c732124bf..5a59ef0dc 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/SimulatorRestAutoConfigurationTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/http/SimulatorRestAutoConfigurationTest.java
@@ -1,14 +1,13 @@
package org.citrusframework.simulator.http;
import org.citrusframework.report.MessageListeners;
+import org.citrusframework.simulator.endpoint.SimulatorEndpointAdapter;
import org.citrusframework.simulator.listener.SimulatorMessageListener;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.context.ApplicationContext;
-import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.SimpleUrlHandlerMapping;
@@ -19,6 +18,7 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.doReturn;
+import static org.springframework.test.util.ReflectionTestUtils.invokeMethod;
/**
* @author Thorsten Schlathoelter
@@ -27,22 +27,22 @@
class SimulatorRestAutoConfigurationTest {
@Mock
- SimulatorRestConfigurationProperties simulatorRestConfigurationProperties;
+ private MessageListeners messageListeners;
@Mock
- SimulatorRestAdapter simulatorRestAdapter;
+ private SimulatorEndpointAdapter simulatorRestEndpointAdapter;
@Mock
- MessageListeners messageListeners;
+ private SimulatorMessageListener simulatorMessageListener;
@Mock
- SimulatorMessageListener simulatorMessageListener;
+ private SimulatorRestAdapter simulatorRestAdapter;
@Mock
- ApplicationContext applicationContext;
+ private SimulatorRestConfigurationProperties simulatorRestConfigurationProperties;
@InjectMocks
- SimulatorRestAutoConfiguration simulatorRestAutoConfiguration;
+ private SimulatorRestAutoConfiguration simulatorRestAutoConfiguration;
@Test
void shouldHandleSingleUrlMappings() {
@@ -52,28 +52,24 @@ void shouldHandleSingleUrlMappings() {
doReturn(new HandlerInterceptor[] {}).when(simulatorRestAdapter).interceptors();
- assertEquals(List.of("/services/rest/**"),
- ReflectionTestUtils.invokeMethod(simulatorRestAutoConfiguration, "getUrlMappings"));
+ assertEquals(List.of("/services/rest/**"), invokeMethod(simulatorRestAutoConfiguration, "getUrlMappings"));
assertEquals("[/services/rest/*]", simulatorRestAutoConfiguration.requestCachingFilter().getUrlPatterns().toString());
- Map simulatorRestHandlerMapping =
- ((SimpleUrlHandlerMapping) simulatorRestAutoConfiguration.simulatorRestHandlerMapping(applicationContext, messageListeners, simulatorMessageListener)).getUrlMap();
+ Map simulatorRestHandlerMapping = ((SimpleUrlHandlerMapping) simulatorRestAutoConfiguration.simulatorRestHandlerMapping(messageListeners, simulatorRestEndpointAdapter, simulatorMessageListener)).getUrlMap();
assertThat(simulatorRestHandlerMapping).containsOnlyKeys("/services/rest/**");
}
@Test
void shouldHandleMultipleUrlMappings() {
- doReturn(List.of("/services/rest1/**", "/services/rest2/**")).when(simulatorRestAdapter)
+ doReturn(List.of("/services/rest1/**", "/services/rest2/**"))
+ .when(simulatorRestAdapter)
.urlMappings(simulatorRestConfigurationProperties);
doReturn(new HandlerInterceptor[] {}).when(simulatorRestAdapter).interceptors();
- assertEquals(List.of("/services/rest1/**", "/services/rest2/**"),
- ReflectionTestUtils.invokeMethod(simulatorRestAutoConfiguration, "getUrlMappings"));
- assertEquals(
- Set.of("/services/rest1/*", "/services/rest2/*"), simulatorRestAutoConfiguration.requestCachingFilter().getUrlPatterns());
+ assertEquals(List.of("/services/rest1/**", "/services/rest2/**"), invokeMethod(simulatorRestAutoConfiguration, "getUrlMappings"));
+ assertEquals(Set.of("/services/rest1/*", "/services/rest2/*"), simulatorRestAutoConfiguration.requestCachingFilter().getUrlPatterns());
- Map simulatorRestHandlerMapping =
- ((SimpleUrlHandlerMapping) simulatorRestAutoConfiguration.simulatorRestHandlerMapping(applicationContext, messageListeners, simulatorMessageListener)).getUrlMap();
+ Map simulatorRestHandlerMapping = ((SimpleUrlHandlerMapping) simulatorRestAutoConfiguration.simulatorRestHandlerMapping(messageListeners, simulatorRestEndpointAdapter, simulatorMessageListener)).getUrlMap();
assertThat(simulatorRestHandlerMapping).containsOnlyKeys("/services/rest1/**", "/services/rest2/**");
}
}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenarioTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenarioTest.java
new file mode 100644
index 000000000..1801f6ca6
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenarioTest.java
@@ -0,0 +1,27 @@
+package org.citrusframework.simulator.scenario;
+
+import org.citrusframework.TestCaseRunner;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+@ExtendWith({MockitoExtension.class})
+class AbstractSimulatorScenarioTest {
+
+ @Mock
+ private TestCaseRunner testCaseRunnerMock;
+
+ @Test
+ void isTestCaseRunnerAware() {
+ var fixture = new AbstractSimulatorScenario() {
+ };
+
+ fixture.setTestCaseRunner(testCaseRunnerMock);
+
+ assertThat(fixture.getTestCaseRunner())
+ .isEqualTo(testCaseRunnerMock);
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorTestScenario.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorTestScenario.java
new file mode 100644
index 000000000..09dc577e9
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorTestScenario.java
@@ -0,0 +1,28 @@
+package org.citrusframework.simulator.scenario;
+
+import jakarta.annotation.Nullable;
+import org.apache.commons.lang3.NotImplementedException;
+import org.citrusframework.TestCaseRunner;
+
+public class AbstractSimulatorTestScenario implements SimulatorScenario {
+
+ @Override
+ public ScenarioEndpoint getScenarioEndpoint() {
+ throw notImplementedException();
+ }
+
+ @Nullable
+ @Override
+ public TestCaseRunner getTestCaseRunner() {
+ throw notImplementedException();
+ }
+
+ @Override
+ public void setTestCaseRunner(TestCaseRunner testCaseRunner) {
+ throw notImplementedException();
+ }
+
+ private static NotImplementedException notImplementedException() {
+ return new NotImplementedException("Not implemented for this testcase!");
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/ScenarioEndpointTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/ScenarioEndpointTest.java
new file mode 100644
index 000000000..89f7911cb
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/ScenarioEndpointTest.java
@@ -0,0 +1,51 @@
+package org.citrusframework.simulator.scenario;
+
+import org.citrusframework.message.Message;
+import org.citrusframework.simulator.endpoint.SimulationFailedUnexpectedlyException;
+import org.citrusframework.simulator.exception.SimulatorException;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import java.util.concurrent.CompletableFuture;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.Mockito.mock;
+
+class ScenarioEndpointTest {
+
+ private ScenarioEndpoint fixture;
+
+ @BeforeEach
+ void beforeEachSetup() {
+ fixture = new ScenarioEndpoint(null);
+ }
+
+ @Nested
+ class Fail {
+
+ @Test
+ void throwsExceptionWhenNoResponseFuturePresent() {
+ assertThatThrownBy(() -> fixture.fail(null))
+ .isInstanceOf(SimulatorException.class)
+ .hasMessage("Failed to process scenario response message - missing response consumer!");
+ }
+
+ @Test
+ void completesResponseFutureIfOneIsPresent() {
+ CompletableFuture responseFuture = new CompletableFuture<>();
+ fixture.add(mock(Message.class), responseFuture);
+
+ var cause = mock(Throwable.class);
+ fixture.fail(cause);
+
+ assertThat(responseFuture)
+ .isCompleted();
+ assertThat(responseFuture.join())
+ .isInstanceOf(SimulationFailedUnexpectedlyException.class)
+ .extracting(Message::getPayload)
+ .isEqualTo(cause);
+ }
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/SimulatorScenarioTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/SimulatorScenarioTest.java
new file mode 100644
index 000000000..a3b586c5e
--- /dev/null
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/SimulatorScenarioTest.java
@@ -0,0 +1,106 @@
+package org.citrusframework.simulator.scenario;
+
+import jakarta.annotation.Nullable;
+import org.apache.commons.lang3.NotImplementedException;
+import org.citrusframework.DefaultTestCaseRunner;
+import org.citrusframework.TestCaseRunner;
+import org.citrusframework.context.TestContext;
+import org.citrusframework.exceptions.CitrusRuntimeException;
+import org.citrusframework.simulator.exception.SimulatorException;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentCaptor.captor;
+import static org.mockito.Mockito.doReturn;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoInteractions;
+
+@ExtendWith({MockitoExtension.class})
+class SimulatorScenarioTest {
+
+ @Mock
+ private ScenarioEndpoint scenarioEndpointMock;
+
+ @Nested
+ class RegisterException {
+
+ @Test
+ void withoutRegisteredTestCaseRunner() {
+ var fixture = new TestSimulatorScenario(null);
+
+ var cause = mock(SimulatorException.class);
+
+ fixture.registerException(cause);
+
+ verify(scenarioEndpointMock).fail(cause);
+ }
+
+ @Test
+ void withRandomTestCaseRunnerImplementation() {
+ var testCaseRunnerMock = mock(TestCaseRunner.class);
+
+ var fixture = new TestSimulatorScenario(testCaseRunnerMock);
+
+ var cause = mock(SimulatorException.class);
+
+ fixture.registerException(cause);
+
+ verify(scenarioEndpointMock).fail(cause);
+ verifyNoInteractions(testCaseRunnerMock);
+ }
+
+ @Test
+ void withDefaultTestCaseRunner() {
+ var testCaseRunnerMock = mock(DefaultTestCaseRunner.class);
+ var testContextMock = mock(TestContext.class);
+ doReturn(testContextMock).when(testCaseRunnerMock).getContext();
+
+ var fixture = new TestSimulatorScenario(testCaseRunnerMock);
+
+ var cause = mock(CitrusRuntimeException.class);
+
+ fixture.registerException(cause);
+
+ verify(scenarioEndpointMock).fail(cause);
+
+ ArgumentCaptor exceptionArgumentCaptor = captor();
+ verify(testContextMock).addException(exceptionArgumentCaptor.capture());
+
+ assertThat(exceptionArgumentCaptor.getValue())
+ .cause()
+ .isEqualTo(cause);
+ }
+ }
+
+ private class TestSimulatorScenario implements SimulatorScenario {
+
+ private @Nullable
+ final TestCaseRunner testCaseRunner;
+
+ private TestSimulatorScenario(@Nullable TestCaseRunner testCaseRunner) {
+ this.testCaseRunner = testCaseRunner;
+ }
+
+ @Override
+ public ScenarioEndpoint getScenarioEndpoint() {
+ return scenarioEndpointMock;
+ }
+
+ @Nullable
+ @Override
+ public TestCaseRunner getTestCaseRunner() {
+ return testCaseRunner;
+ }
+
+ @Override
+ public void setTestCaseRunner(TestCaseRunner testCaseRunner) {
+ throw new NotImplementedException("Not implemented for this testcase!");
+ }
+ }
+}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappersTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappersTest.java
index 708672775..ae3054714 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappersTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/mapper/ScenarioMappersTest.java
@@ -1,19 +1,3 @@
-/*
- * Copyright 2006-2019 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.scenario.mapper;
import org.citrusframework.exceptions.CitrusRuntimeException;
@@ -36,9 +20,6 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
-/**
- * @author Christoph Deppisch
- */
class ScenarioMappersTest {
private static final String DEFAULT_SCENARIO = "default";
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java
index cb10beb34..7d995df9b 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java
@@ -16,11 +16,10 @@
package org.citrusframework.simulator.service.impl;
-import org.apache.commons.lang3.NotImplementedException;
import org.citrusframework.simulator.events.ScenariosReloadedEvent;
import org.citrusframework.simulator.model.ScenarioParameter;
+import org.citrusframework.simulator.scenario.AbstractSimulatorTestScenario;
import org.citrusframework.simulator.scenario.Scenario;
-import org.citrusframework.simulator.scenario.ScenarioEndpoint;
import org.citrusframework.simulator.scenario.ScenarioStarter;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.citrusframework.simulator.scenario.Starter;
@@ -159,34 +158,19 @@ void lookupScenarioParametersReturnsEmptyListForInvalidScenarioNames() {
}
@Scenario(SCENARIO_NAME)
- private static class TestSimulatorScenario implements SimulatorScenario {
-
- @Override
- public ScenarioEndpoint getScenarioEndpoint() {
- throw new NotImplementedException();
- }
+ private static class TestSimulatorScenario extends AbstractSimulatorTestScenario {
}
@Starter(STARTER_NAME)
- private static class TetsScenarioStarter implements ScenarioStarter {
+ private static class TetsScenarioStarter extends AbstractSimulatorTestScenario implements ScenarioStarter {
@Override
public List getScenarioParameters() {
return List.of(SCENARIO_PARAMETER);
}
-
- @Override
- public ScenarioEndpoint getScenarioEndpoint() {
- throw new NotImplementedException();
- }
}
@Starter("ScenarioLookupServiceImplTest#invalidTestScenarioStarter")
- private static class InvalidTestSimulatorScenario implements SimulatorScenario {
-
- @Override
- public ScenarioEndpoint getScenarioEndpoint() {
- throw new NotImplementedException();
- }
+ private static class InvalidTestSimulatorScenario extends AbstractSimulatorTestScenario {
}
}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceTest.java
index 5153016a4..7caa8aa5e 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/AsyncScenarioExecutorServiceTest.java
@@ -3,6 +3,7 @@
import org.citrusframework.TestCase;
import org.citrusframework.report.TestListeners;
import org.citrusframework.simulator.config.SimulatorConfigurationProperties;
+import org.citrusframework.simulator.exception.SimulatorException;
import org.citrusframework.simulator.model.ScenarioExecution;
import org.citrusframework.simulator.scenario.ScenarioRunner;
import org.citrusframework.simulator.scenario.SimulatorScenario;
@@ -13,7 +14,6 @@
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.context.ApplicationContext;
import org.springframework.context.event.ContextClosedEvent;
import java.util.concurrent.ExecutorService;
@@ -21,14 +21,11 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentCaptor.captor;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -39,9 +36,6 @@ class AsyncScenarioExecutorServiceTest extends ScenarioExecutorServiceTest {
private static final int THREAD_POOL_SIZE = 1234;
- @Mock
- private ApplicationContext applicationContextMock;
-
@Mock
private ExecutorService executorServiceMock;
@@ -79,7 +73,8 @@ void constructorCreatesThreadPool() {
@Test
void runSimulatorScenarioByName() {
- var simulatorScenarioMock = mock(SimulatorScenario.class);
+ var simulatorScenarioMock = getSimulatorScenarioMock();
+
doReturn(simulatorScenarioMock).when(applicationContextMock).getBean(scenarioName, SimulatorScenario.class);
Long executionId = mockScenarioExecutionCreation();
@@ -89,7 +84,7 @@ void runSimulatorScenarioByName() {
assertEquals(executionId, result);
ArgumentCaptor scenarioRunnableArgumentCaptor = captor();
- verify(executorServiceMock).submit(scenarioRunnableArgumentCaptor.capture());
+ verify(executorServiceMock).execute(scenarioRunnableArgumentCaptor.capture());
// Now, we need more mocks!
mockCitrusTestContext();
@@ -97,8 +92,8 @@ void runSimulatorScenarioByName() {
// This invokes the scenario execution with the captured runnable
scenarioRunnableArgumentCaptor.getValue().run();
- verify(simulatorScenarioMock).getScenarioEndpoint();
- verify(simulatorScenarioMock).run(any(ScenarioRunner.class));
+ verifyTestCaseRunnerHasBeenConfigured(simulatorScenarioMock);
+ verifyScenarioHasBeenRunWithScenarioRunner(simulatorScenarioMock);
verifyNoMoreInteractions(simulatorScenarioMock);
}
@@ -106,30 +101,31 @@ void runSimulatorScenarioByName() {
void runScenarioDirectly() {
Long executionId = mockScenarioExecutionCreation();
- var simulatorScenario = spy(new CustomSimulatorScenario());
+ var simulatorScenarioMock = getSimulatorScenarioMock();
// Note that this does not actually "run" the scenario (because of the mocked executor service), it just creates it.
- Long result = fixture.run(simulatorScenario, scenarioName, parameters);
- verifyScenarioExecution(executionId, result, simulatorScenario);
+ Long result = fixture.run(simulatorScenarioMock, scenarioName, parameters);
+ verifyScenarioExecution(executionId, result, simulatorScenarioMock);
- assertTrue(isCustomScenarioExecuted());
+ verifyScenarioHasBeenRunWithScenarioRunner(simulatorScenarioMock);
}
@Test
- void exceptionDuringExecutionWillBeCatched() {
+ void exceptionDuringExecutionWillBeCaught() {
Long executionId = mockScenarioExecutionCreation();
- var simulatorScenario = spy(new CustomSimulatorScenario());
+ var simulatorScenarioMock = getSimulatorScenarioMock();
// Invoke exception
- doThrow(new IllegalArgumentException()).when(simulatorScenario).run(any(ScenarioRunner.class));
+ var cause = mock(SimulatorException.class);
+ doThrow(cause).when(simulatorScenarioMock).run(any(ScenarioRunner.class));
// Note that this does not actually "run" the scenario (because of the mocked executor service), it just creates it.
// This method-invocation may not throw, despite the above exception!
- Long result = fixture.run(simulatorScenario, scenarioName, parameters);
- verifyScenarioExecution(executionId, result, simulatorScenario);
+ Long result = fixture.run(simulatorScenarioMock, scenarioName, parameters);
+ verifyScenarioExecution(executionId, result, simulatorScenarioMock);
- assertFalse(isCustomScenarioExecuted());
+ verify(simulatorScenarioMock).registerException(cause);
}
@Test
@@ -144,11 +140,11 @@ void shutdownExecutorOnApplicationContextEvent() {
verify(executorServiceMock).shutdownNow();
}
- private void verifyScenarioExecution(Long executionId, Long result, AsyncScenarioExecutorServiceTest.CustomSimulatorScenario simulatorScenario) {
+ private void verifyScenarioExecution(Long executionId, Long result, SimulatorScenario simulatorScenario) {
assertEquals(executionId, result);
ArgumentCaptor scenarioRunnableArgumentCaptor = captor();
- verify(executorServiceMock).submit(scenarioRunnableArgumentCaptor.capture());
+ verify(executorServiceMock).execute(scenarioRunnableArgumentCaptor.capture());
// Now, we need more mocks!
var testContextMock = mockCitrusTestContext();
@@ -158,11 +154,11 @@ private void verifyScenarioExecution(Long executionId, Long result, AsyncScenari
// This invokes the scenario execution with the captured runnable
scenarioRunnableArgumentCaptor.getValue().run();
- ArgumentCaptor scenarioRunnerArgumentCaptor = ArgumentCaptor.forClass(ScenarioRunner.class);
+ ArgumentCaptor scenarioRunnerArgumentCaptor = captor();
verify(simulatorScenario, times(1)).run(scenarioRunnerArgumentCaptor.capture());
var scenarioRunner = scenarioRunnerArgumentCaptor.getValue();
- assertEquals(scenarioEndpointMock, scenarioRunner.scenarioEndpoint());
+ assertEquals(scenarioEndpointMock, scenarioRunner.getScenarioEndpoint());
assertEquals(executionId, scenarioRunner.getTestCaseRunner().getTestCase().getVariableDefinitions().get(ScenarioExecution.EXECUTION_ID));
verify(testListenersMock).onTestStart(any(TestCase.class));
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceIT.java
index 4b26d3548..b38a769df 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceIT.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceIT.java
@@ -1,19 +1,3 @@
-/*
- * Copyright 2024 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.service.runner;
import org.citrusframework.simulator.IntegrationTest;
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceTest.java
index 5875fec41..28389e7bc 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/DefaultScenarioExecutorServiceTest.java
@@ -1,7 +1,9 @@
package org.citrusframework.simulator.service.runner;
+import jakarta.annotation.Nullable;
import org.citrusframework.TestCase;
import org.citrusframework.report.TestListeners;
+import org.citrusframework.simulator.exception.SimulatorException;
import org.citrusframework.simulator.model.ScenarioExecution;
import org.citrusframework.simulator.scenario.ScenarioRunner;
import org.citrusframework.simulator.scenario.SimulatorScenario;
@@ -10,19 +12,17 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
-import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
-import org.springframework.context.ApplicationContext;
+import static java.util.Objects.isNull;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentCaptor.captor;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoMoreInteractions;
@@ -30,8 +30,6 @@
@ExtendWith(MockitoExtension.class)
class DefaultScenarioExecutorServiceTest extends ScenarioExecutorServiceTest {
- @Mock
- private ApplicationContext applicationContextMock;
private DefaultScenarioExecutorService fixture;
@@ -50,7 +48,7 @@ void isScenarioExecutorService() {
@Test
void runSimulatorScenarioByName() {
- var simulatorScenarioMock = mock(SimulatorScenario.class);
+ var simulatorScenarioMock = getSimulatorScenarioMock();
doReturn(simulatorScenarioMock).when(applicationContextMock).getBean(scenarioName, SimulatorScenario.class);
Long executionId = mockScenarioExecutionCreation();
@@ -61,8 +59,8 @@ void runSimulatorScenarioByName() {
Long result = fixture.run(scenarioName, parameters);
assertEquals(executionId, result);
- verify(simulatorScenarioMock).getScenarioEndpoint();
- verify(simulatorScenarioMock).run(any(ScenarioRunner.class));
+ verifyTestCaseRunnerHasBeenConfigured(simulatorScenarioMock);
+ verifyScenarioHasBeenRunWithScenarioRunner(simulatorScenarioMock);
verifyNoMoreInteractions(simulatorScenarioMock);
}
@@ -70,48 +68,50 @@ void runSimulatorScenarioByName() {
void runScenarioDirectly() {
Long executionId = mockScenarioExecutionCreation();
- var simulatorScenario = spy(new CustomSimulatorScenario());
+ var simulatorScenarioMock = getSimulatorScenarioMock();
var testContextMock = mockCitrusTestContext();
var testListenersMock = mock(TestListeners.class);
doReturn(testListenersMock).when(testContextMock).getTestListeners();
// Note that this does not actually "run" the scenario (because of the mocked executor service), it just creates it.
- Long result = fixture.run(simulatorScenario, scenarioName, parameters);
- verifyScenarioExecution(executionId, result, simulatorScenario, testListenersMock);
+ Long result = fixture.run(simulatorScenarioMock, scenarioName, parameters);
+ verifyScenarioExecution(executionId, result, simulatorScenarioMock, testListenersMock);
- assertTrue(isCustomScenarioExecuted());
+ verifyScenarioHasBeenRunWithScenarioRunner(simulatorScenarioMock);
}
@Test
- void exceptionDuringExecutionWillBeCatched() {
+ void exceptionDuringExecutionWillBeCaught() {
Long executionId = mockScenarioExecutionCreation();
- var simulatorScenario = spy(new CustomSimulatorScenario());
+ var simulatorScenarioMock = getSimulatorScenarioMock();
var testContextMock = mockCitrusTestContext();
var testListenersMock = mock(TestListeners.class);
doReturn(testListenersMock).when(testContextMock).getTestListeners();
// Invoke exception
- doThrow(new IllegalArgumentException()).when(simulatorScenario).run(any(ScenarioRunner.class));
+ var cause = mock(SimulatorException.class);
+ doThrow(cause).when(simulatorScenarioMock).run(any(ScenarioRunner.class));
- // Note that this does not actually "run" the scenario (because of the mocked executor service), it just creates it.
- // This method-invocation may not throw, despite the above exception!
- Long result = fixture.run(simulatorScenario, scenarioName, parameters);
- verifyScenarioExecution(executionId, result, simulatorScenario, testListenersMock);
+ assertThatThrownBy(() -> fixture.run(simulatorScenarioMock, scenarioName, parameters))
+ .isEqualTo(cause);
+ verifyScenarioExecution(executionId, null, simulatorScenarioMock, testListenersMock);
- assertFalse(isCustomScenarioExecuted());
+ verify(simulatorScenarioMock).registerException(cause);
}
- private void verifyScenarioExecution(Long executionId, Long result, CustomSimulatorScenario simulatorScenario, TestListeners testListenersMock) {
- assertEquals(executionId, result);
+ private void verifyScenarioExecution(Long executionId, @Nullable Long result, SimulatorScenario simulatorScenario, TestListeners testListenersMock) {
+ if (!isNull(result)) {
+ assertEquals(executionId, result);
+ }
- ArgumentCaptor scenarioRunnerArgumentCaptor = ArgumentCaptor.forClass(ScenarioRunner.class);
+ ArgumentCaptor scenarioRunnerArgumentCaptor = captor();
verify(simulatorScenario, times(1)).run(scenarioRunnerArgumentCaptor.capture());
var scenarioRunner = scenarioRunnerArgumentCaptor.getValue();
- assertEquals(scenarioEndpointMock, scenarioRunner.scenarioEndpoint());
+ assertEquals(scenarioEndpointMock, scenarioRunner.getScenarioEndpoint());
assertEquals(executionId, scenarioRunner.getTestCaseRunner().getTestCase().getVariableDefinitions().get(ScenarioExecution.EXECUTION_ID));
verify(testListenersMock).onTestStart(any(TestCase.class));
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/ScenarioExecutorServiceTest.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/ScenarioExecutorServiceTest.java
index cb40c3be0..c8f86bc75 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/ScenarioExecutorServiceTest.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/service/runner/ScenarioExecutorServiceTest.java
@@ -2,6 +2,8 @@
import org.citrusframework.Citrus;
import org.citrusframework.CitrusContext;
+import org.citrusframework.DefaultTestCaseRunner;
+import org.citrusframework.TestCaseRunner;
import org.citrusframework.context.TestContext;
import org.citrusframework.report.TestListeners;
import org.citrusframework.simulator.model.ScenarioExecution;
@@ -10,21 +12,25 @@
import org.citrusframework.simulator.scenario.ScenarioRunner;
import org.citrusframework.simulator.scenario.SimulatorScenario;
import org.citrusframework.simulator.service.ScenarioExecutionService;
-import org.junit.jupiter.api.Assertions;
+import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
+import org.springframework.context.ApplicationContext;
import java.util.List;
-import java.util.concurrent.atomic.AtomicBoolean;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentCaptor.captor;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
abstract class ScenarioExecutorServiceTest {
protected static ScenarioEndpoint scenarioEndpointMock;
- private static AtomicBoolean customScenarioExecuted;
+ @Mock
+ protected ApplicationContext applicationContextMock;
@Mock
protected Citrus citrusMock;
@@ -44,13 +50,23 @@ abstract class ScenarioExecutorServiceTest {
.build()
);
- protected void beforeEachSetup() {
- scenarioEndpointMock = mock(ScenarioEndpoint.class);
- customScenarioExecuted = new AtomicBoolean(false);
+ protected static SimulatorScenario getSimulatorScenarioMock() {
+ var simulatorScenarioMock = mock(SimulatorScenario.class);
+ doReturn(scenarioEndpointMock).when(simulatorScenarioMock).getScenarioEndpoint();
+ return simulatorScenarioMock;
+ }
+
+ protected static void verifyTestCaseRunnerHasBeenConfigured(SimulatorScenario simulatorScenarioMock) {
+ ArgumentCaptor runnerArgumentCaptor = captor();
+ verify(simulatorScenarioMock).setTestCaseRunner(runnerArgumentCaptor.capture());
+
+ assertThat(runnerArgumentCaptor.getValue())
+ .isInstanceOf(DefaultTestCaseRunner.class)
+ .hasNoNullFieldsOrProperties();
}
- protected boolean isCustomScenarioExecuted() {
- return customScenarioExecuted.get();
+ protected void beforeEachSetup() {
+ scenarioEndpointMock = mock(ScenarioEndpoint.class);
}
protected Long mockScenarioExecutionCreation() {
@@ -72,24 +88,15 @@ protected TestContext mockCitrusTestContext() {
return testContextMock;
}
- protected static class BaseCustomSimulatorScenario implements SimulatorScenario {
-
- @Override
- public ScenarioEndpoint getScenarioEndpoint() {
- return scenarioEndpointMock;
- }
-
- @Override
- public void run(ScenarioRunner runner) {
- Assertions.fail("This method should never be called");
- }
- }
-
- protected static class CustomSimulatorScenario extends BaseCustomSimulatorScenario {
+ protected void verifyScenarioHasBeenRunWithScenarioRunner(SimulatorScenario simulatorScenarioMock) {
+ ArgumentCaptor scenarioRunnerArgumentCaptor = captor();
+ verify(simulatorScenarioMock).run(scenarioRunnerArgumentCaptor.capture());
- @Override
- public void run(ScenarioRunner runner) {
- customScenarioExecuted.set(true);
- }
+ assertThat(scenarioRunnerArgumentCaptor.getValue())
+ .hasNoNullFieldsOrProperties()
+ .satisfies(
+ r -> assertThat(r.getScenarioEndpoint()).isEqualTo(scenarioEndpointMock),
+ r -> assertThat(r.getApplicationContext()).isEqualTo(applicationContextMock)
+ );
}
}
diff --git a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java
index a73ca2ed4..090a732cf 100644
--- a/simulator-spring-boot/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java
+++ b/simulator-spring-boot/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java
@@ -123,7 +123,7 @@ void getTestSimulatorScenario() throws Exception {
.perform(get(ENTITY_API_URL + "?nameContains=Simulator"))
.andExpect(status().isOk())
.andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE))
- .andExpect(jsonPath("$.length()").value(equalTo(1)))
+ .andExpect(jsonPath("$.length()").value(equalTo(4)))
.andExpect(jsonPath("$.[*]", hasItem(
allOf(
hasEntry("name", SCENARIO_NAME),
diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html b/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html
index 2ac90defc..a2b893adc 100644
--- a/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html
+++ b/simulator-ui/src/main/webapp/app/entities/scenario-execution/list/scenario-execution.component.html
@@ -72,8 +72,8 @@
{{ scenarioExecution.startDate | formatMediumDatetime }} |
{{ scenarioExecution.endDate | formatMediumDatetime }} |
- {{
- scenarioExecution.testResult?.status
+ {{
+ scenarioExecution.testResult?.status ?? 'RUNNING'
}}
|
{{ scenarioExecution.testResult?.errorMessage }} |
diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.spec.ts b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.spec.ts
index 9e1fae537..01145be08 100644
--- a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.spec.ts
+++ b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.spec.ts
@@ -4,13 +4,14 @@ import {
STATUS_UNKNOWN,
STATUS_SUCCESS,
STATUS_FAILURE,
- STATUS_SKIP,
+ STATUS_SKIP, STATUS_RUNNING,
} from './test-result.model';
describe('TestResult Status', () => {
describe('testResultStatusFromName', () => {
it('should return the correct status for a valid name', () => {
expect(testResultStatusFromName('UNKNOWN')).toEqual(STATUS_UNKNOWN);
+ expect(testResultStatusFromName('RUNNING')).toEqual(STATUS_RUNNING);
expect(testResultStatusFromName('SUCCESS')).toEqual(STATUS_SUCCESS);
expect(testResultStatusFromName('FAILURE')).toEqual(STATUS_FAILURE);
expect(testResultStatusFromName('SKIP')).toEqual(STATUS_SKIP);
@@ -24,6 +25,7 @@ describe('TestResult Status', () => {
describe('testResultStatusFromId', () => {
it('should return the correct status for a valid id', () => {
expect(testResultStatusFromId(0)).toEqual(STATUS_UNKNOWN);
+ expect(testResultStatusFromId(-1)).toEqual(STATUS_RUNNING);
expect(testResultStatusFromId(1)).toEqual(STATUS_SUCCESS);
expect(testResultStatusFromId(2)).toEqual(STATUS_FAILURE);
expect(testResultStatusFromId(3)).toEqual(STATUS_SKIP);
diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts
index 74dc0b177..b4229b884 100644
--- a/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts
+++ b/simulator-ui/src/main/webapp/app/entities/test-result/test-result.model.ts
@@ -14,15 +14,16 @@ export interface ITestResult {
export interface ITestResultStatus {
id: number;
- name: 'UNKNOWN' | 'SUCCESS' | 'FAILURE' | 'SKIP';
+ name: 'UNKNOWN' | 'RUNNING' | 'SUCCESS' | 'FAILURE' | 'SKIP';
}
+export const STATUS_RUNNING: ITestResultStatus = { id: -1, name: 'RUNNING' };
export const STATUS_UNKNOWN: ITestResultStatus = { id: 0, name: 'UNKNOWN' };
export const STATUS_SUCCESS: ITestResultStatus = { id: 1, name: 'SUCCESS' };
export const STATUS_FAILURE: ITestResultStatus = { id: 2, name: 'FAILURE' };
export const STATUS_SKIP: ITestResultStatus = { id: 3, name: 'SKIP' };
-const ALL_STATUS = [STATUS_UNKNOWN, STATUS_SUCCESS, STATUS_FAILURE, STATUS_SKIP];
+const ALL_STATUS = [STATUS_UNKNOWN, STATUS_RUNNING, STATUS_SUCCESS, STATUS_FAILURE, STATUS_SKIP];
export const testResultStatusFromName = (name: string): ITestResultStatus => ALL_STATUS.find(v => v.name === name) ?? STATUS_UNKNOWN;
diff --git a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java
index 0ae348048..22b4cbe51 100644
--- a/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java
+++ b/simulator-ui/src/test/java/org/citrusframework/simulator/ui/test/TestApplication.java
@@ -20,7 +20,6 @@ public static void main(String[] args) {
@Bean("DEFAULT_SCENARIO")
public SimulatorScenario defaultScenario() {
return new AbstractSimulatorScenario() {
-
};
}
}