From 4a4ffcd70c1272abef72741e6a23a016a521fc53 Mon Sep 17 00:00:00 2001 From: Timon Borter Date: Mon, 13 May 2024 17:17:23 +0200 Subject: [PATCH] fix(simulator-spring-boot): indicate simulation failure with http status --- .../sample/scenario/FailScenario.java | 26 +-- .../sample/scenario/ThrowScenario.java | 48 ++++++ .../simulator/SimulatorRestIT.java | 45 ++++- ...SimulationFailedUnexpectedlyException.java | 41 +++++ .../endpoint/SimulatorEndpointAdapter.java | 157 ++++++++---------- .../SimulatorEndpointAutoConfiguration.java | 52 +++--- .../endpoint/SimulatorEndpointPoller.java | 36 ++-- .../endpoint/SimulatorSoapEndpointPoller.java | 16 +- .../simulator/http/HttpOperationScenario.java | 19 ++- .../http/SimulatorRestAutoConfiguration.java | 67 ++++---- .../jms/SimulatorJmsAutoConfiguration.java | 47 +++--- .../listener/SimulatorStatusListener.java | 7 +- .../scenario/AbstractSimulatorScenario.java | 36 ++-- .../simulator/scenario/ScenarioEndpoint.java | 30 +++- .../simulator/scenario/ScenarioRunner.java | 25 +-- .../simulator/scenario/SimulatorScenario.java | 31 +++- .../runner/AsyncScenarioExecutorService.java | 4 +- .../DefaultScenarioExecutorService.java | 25 ++- .../SimulatorWebServiceAutoConfiguration.java | 37 +++-- .../simulator/ws/WsdlOperationScenario.java | 19 ++- .../META-INF/citrus-simulator.properties | 1 + ...synchronousSimulatorEndpointAdapterIT.java | 26 +++ ...lationFailedUnexpectedlyExceptionTest.java | 27 +++ .../endpoint/SimulatorEndpointAdapterIT.java | 94 +++++++++++ ...SynchronousSimulatorEndpointAdapterIT.java | 23 +++ .../events/ScenariosReloadedEventIT.java | 10 +- .../HttpRequestAnnotationMatcherTest.java | 57 +++---- ...HttpRequestAnnotationScenarioMapperIT.java | 19 --- ...tpRequestAnnotationScenarioMapperTest.java | 39 ++--- .../SimulatorRestAutoConfigurationTest.java | 34 ++-- .../AbstractSimulatorScenarioTest.java | 27 +++ .../AbstractSimulatorTestScenario.java | 28 ++++ .../scenario/ScenarioEndpointTest.java | 51 ++++++ .../scenario/SimulatorScenarioTest.java | 106 ++++++++++++ .../scenario/mapper/ScenarioMappersTest.java | 19 --- .../impl/ScenarioLookupServiceImplTest.java | 24 +-- .../AsyncScenarioExecutorServiceTest.java | 46 +++-- .../DefaultScenarioExecutorServiceIT.java | 16 -- .../DefaultScenarioExecutorServiceTest.java | 52 +++--- .../runner/ScenarioExecutorServiceTest.java | 59 ++++--- .../web/rest/ScenarioResourceIT.java | 2 +- .../list/scenario-execution.component.html | 4 +- .../test-result/test-result.model.spec.ts | 4 +- .../entities/test-result/test-result.model.ts | 5 +- .../simulator/ui/test/TestApplication.java | 1 - 45 files changed, 1004 insertions(+), 538 deletions(-) create mode 100644 simulator-samples/sample-rest/src/main/java/org/citrusframework/simulator/sample/scenario/ThrowScenario.java create mode 100644 simulator-spring-boot/src/main/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyException.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/AsynchronousSimulatorEndpointAdapterIT.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulationFailedUnexpectedlyExceptionTest.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SimulatorEndpointAdapterIT.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/endpoint/SynchronousSimulatorEndpointAdapterIT.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorScenarioTest.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/AbstractSimulatorTestScenario.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/ScenarioEndpointTest.java create mode 100644 simulator-spring-boot/src/test/java/org/citrusframework/simulator/scenario/SimulatorScenarioTest.java 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() { - }; } }