From efac87c7bec059667395b9da5ba7af0a69013d7b Mon Sep 17 00:00:00 2001 From: Timon Borter Date: Mon, 30 Oct 2023 21:47:43 +0100 Subject: [PATCH] feat: finishing touch on rest api and ui * implement scenario REST API * add scenario control to frontend * enhance navigation, buttons and icons * improve tests for scenario execution --- .../simulator/SimulatorJmsFaxIT.java | 3 +- .../SimulatorWebServiceClientIT.java | 2 +- .../simulator/SimulatorAutoConfiguration.java | 2 - .../controller/ScenarioController.java | 113 ----------- .../events/ScenariosReloadedEvent.java | 26 +++ .../simulator/service/ActivityService.java | 26 +-- .../service/MessageHeaderQueryService.java | 16 ++ .../service/MessageHeaderService.java | 16 ++ .../service/MessageQueryService.java | 16 ++ .../simulator/service/MessageService.java | 16 ++ .../simulator/service/QueryService.java | 16 ++ .../service/ScenarioActionQueryService.java | 16 ++ .../service/ScenarioActionService.java | 16 ++ .../ScenarioExecutionQueryService.java | 16 ++ .../service/ScenarioExecutionService.java | 28 +++ .../service/ScenarioExecutorService.java | 137 ++----------- .../service/ScenarioLookupService.java | 104 +++------- .../ScenarioParameterQueryService.java | 16 ++ .../service/ScenarioParameterService.java | 16 ++ .../simulator/service/TemplateService.java | 16 ++ .../service/TestParameterQueryService.java | 16 ++ .../service/TestParameterService.java | 16 ++ .../service/TestResultQueryService.java | 16 ++ .../simulator/service/TestResultService.java | 16 ++ .../simulator/service/TimeProvider.java | 16 ++ .../simulator/service/criteria/Criteria.java | 16 ++ .../service/criteria/MessageCriteria.java | 16 ++ .../criteria/MessageHeaderCriteria.java | 16 ++ .../criteria/ScenarioActionCriteria.java | 16 ++ .../criteria/ScenarioExecutionCriteria.java | 16 ++ .../criteria/ScenarioParameterCriteria.java | 16 ++ .../criteria/TestParameterCriteria.java | 16 ++ .../service/criteria/TestResultCriteria.java | 16 ++ .../service/dto/TestResultByStatus.java | 18 +- .../simulator/service/filter/Filter.java | 18 ++ .../service/filter/InstantFilter.java | 18 ++ .../service/filter/IntegerFilter.java | 18 ++ .../simulator/service/filter/LongFilter.java | 18 ++ .../simulator/service/filter/RangeFilter.java | 18 ++ .../service/filter/StringFilter.java | 18 ++ .../DefaultScenarioExecutorServiceImpl.java | 168 ++++++++++++++++ .../impl/MessageHeaderServiceImpl.java | 16 ++ .../service/impl/MessageServiceImpl.java | 16 ++ .../impl/ScenarioActionServiceImpl.java | 16 ++ .../impl/ScenarioExecutionServiceImpl.java | 46 ++++- .../impl/ScenarioLookupServiceImpl.java | 135 +++++++++++++ .../impl/ScenarioParameterServiceImpl.java | 16 ++ .../impl/TestParameterServiceImpl.java | 16 ++ .../service/impl/TestResultServiceImpl.java | 16 ++ .../simulator/service/impl/TimeProvider.java | 32 +++ .../simulator/template/TemplateHelper.java | 16 ++ .../web/rest/MessageHeaderResource.java | 16 ++ .../simulator/web/rest/MessageResource.java | 16 ++ .../web/rest/ScenarioActionResource.java | 16 ++ .../web/rest/ScenarioExecutionResource.java | 16 ++ .../web/rest/ScenarioParameterResource.java | 16 ++ .../simulator/web/rest/ScenarioResource.java | 128 ++++++++++++ .../web/rest/TestParameterResource.java | 16 ++ .../web/rest/TestResultResource.java | 16 ++ .../simulator/web/util/PaginationUtil.java | 29 ++- .../simulator/web/util/ResponseUtil.java | 16 ++ .../src/main/resources/static/favicon.ico | Bin 16958 -> 15406 bytes .../src/main/resources/static/index.html | 12 +- .../events/ScenariosReloadedEventTest.java | 38 ++++ ...efaultScenarioExecutorServiceImplTest.java | 185 ++++++++++++++++++ .../ScenarioExecutionServiceImplTest.java | 40 +++- .../impl/ScenarioLookupServiceImplTest.java | 167 ++++++++++++++++ .../web/rest/MessageHeaderResourceIT.java | 16 +- .../simulator/web/rest/MessageResourceIT.java | 16 +- .../web/rest/ScenarioActionResourceIT.java | 16 +- .../web/rest/ScenarioExecutionResourceIT.java | 16 +- .../web/rest/ScenarioParameterResourceIT.java | 16 +- .../web/rest/ScenarioResourceIT.java | 73 +++++++ .../web/rest/ScenarioResourceTest.java | 50 +++++ .../web/rest/TestParameterResourceIT.java | 16 +- .../web/rest/TestResultResourceIT.java | 16 +- .../web/util/PaginationUtilTest.java | 37 ++++ simulator-ui/.jhipster/Scenario.json | 32 +++ simulator-ui/.yo-rc.json | 11 +- .../src/main/webapp/app/app-routing.module.ts | 5 + .../webapp/app/config/font-awesome-icons.ts | 8 + .../list/message-header.component.ts | 13 +- .../message/list/message.component.html | 2 +- .../message/list/message.component.ts | 13 +- .../app/entities/message/message.model.ts | 2 +- .../entities/message/message.test-samples.ts | 8 +- .../list/scenario-action.component.ts | 13 +- .../list/scenario-execution.component.html | 14 +- .../list/scenario-execution.component.ts | 13 +- .../scenario-execution.model.ts | 2 +- .../scenario-execution.test-samples.ts | 8 +- .../list/scenario-parameter.component.ts | 13 +- .../scenario-parameter.model.ts | 2 +- .../scenario-parameter.test-samples.ts | 8 +- .../list/test-result.component.html | 4 +- .../entities/test-result/test-result.model.ts | 2 +- .../test-result/test-result.test-samples.ts | 8 +- .../app/layouts/navbar/navbar.component.html | 8 + .../detail/scenario-detail.component.html | 58 ++++++ .../detail/scenario-detail.component.spec.ts | 52 +++++ .../detail/scenario-detail.component.ts | 31 +++ .../app/scenario/list/scenario.component.html | 85 ++++++++ .../scenario/list/scenario.component.spec.ts | 130 ++++++++++++ .../app/scenario/list/scenario.component.ts | 171 ++++++++++++++++ ...-parameter-routing-resolve.service.spec.ts | 101 ++++++++++ ...nario-parameter-routing-resolve.service.ts | 33 ++++ .../webapp/app/scenario/scenario.model.ts | 4 + .../webapp/app/scenario/scenario.routes.ts | 27 +++ .../app/scenario/scenario.test-samples.ts | 20 ++ .../scenario/service/scenario.service.spec.ts | 163 +++++++++++++++ .../app/scenario/service/scenario.service.ts | 67 +++++++ .../src/main/webapp/i18n/en/scenario.json | 23 +++ .../src/main/webapp/i18n/en/testResult.json | 2 +- 113 files changed, 3133 insertions(+), 466 deletions(-) delete mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/controller/ScenarioController.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/events/ScenariosReloadedEvent.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImpl.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TimeProvider.java create mode 100644 simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioResource.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceTest.java create mode 100644 simulator-starter/src/test/java/org/citrusframework/simulator/web/util/PaginationUtilTest.java create mode 100644 simulator-ui/.jhipster/Scenario.json create mode 100644 simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.html create mode 100644 simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html create mode 100644 simulator-ui/src/main/webapp/app/scenario/list/scenario.component.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/list/scenario.component.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/scenario.model.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/scenario.routes.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/scenario.test-samples.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/service/scenario.service.spec.ts create mode 100644 simulator-ui/src/main/webapp/app/scenario/service/scenario.service.ts create mode 100644 simulator-ui/src/main/webapp/i18n/en/scenario.json diff --git a/simulator-samples/sample-jms-fax/src/test/java/org/citrusframework/simulator/SimulatorJmsFaxIT.java b/simulator-samples/sample-jms-fax/src/test/java/org/citrusframework/simulator/SimulatorJmsFaxIT.java index 22501a433..d13fc3b15 100644 --- a/simulator-samples/sample-jms-fax/src/test/java/org/citrusframework/simulator/SimulatorJmsFaxIT.java +++ b/simulator-samples/sample-jms-fax/src/test/java/org/citrusframework/simulator/SimulatorJmsFaxIT.java @@ -69,6 +69,7 @@ @Test @ContextConfiguration(classes = SimulatorJmsFaxIT.EndpointConfig.class) public class SimulatorJmsFaxIT extends TestNGCitrusSpringSupport { + private final PayloadHelper payloadHelper = new PayloadHelper(); @Autowired @@ -227,7 +228,7 @@ public void testUpdateFaxStatusStarter() { $(http() .client(restEndpoint) .send() - .post("/api/scenario/launch/UpdateFaxStatus") + .post("/api/scenarios/UpdateFaxStatus/launch") .message() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(asJson(referenceId.asScenarioParameter(), diff --git a/simulator-samples/sample-ws-client/src/test/java/org/citrusframework/simulator/SimulatorWebServiceClientIT.java b/simulator-samples/sample-ws-client/src/test/java/org/citrusframework/simulator/SimulatorWebServiceClientIT.java index 0593ff5d1..4fd975fdf 100644 --- a/simulator-samples/sample-ws-client/src/test/java/org/citrusframework/simulator/SimulatorWebServiceClientIT.java +++ b/simulator-samples/sample-ws-client/src/test/java/org/citrusframework/simulator/SimulatorWebServiceClientIT.java @@ -73,7 +73,7 @@ public void testHelloRequest() { $(http() .client(restEndpoint) .send() - .post("/api/scenario/launch/HelloStarter") + .post("/api/scenarios/HelloStarter/launch") .message() .contentType(MediaType.APPLICATION_JSON_VALUE) .body(asJson(name.asScenarioParameter()))); diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java b/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java index 68daa3b86..82611ec29 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/SimulatorAutoConfiguration.java @@ -55,8 +55,6 @@ */ @Configuration @ComponentScan(basePackages = { - // TODO: Remove when scenario controller has been migrated - "org.citrusframework.simulator.controller", "org.citrusframework.simulator.web.rest", "org.citrusframework.simulator.listener", "org.citrusframework.simulator.service", diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/ScenarioController.java b/simulator-starter/src/main/java/org/citrusframework/simulator/controller/ScenarioController.java deleted file mode 100644 index e3fe128f3..000000000 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/controller/ScenarioController.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright 2006-2017 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.controller; - -import lombok.Data; -import lombok.NoArgsConstructor; -import org.citrusframework.simulator.model.ScenarioParameter; -import org.citrusframework.simulator.service.ScenarioExecutorService; -import org.citrusframework.simulator.service.ScenarioLookupService; -import org.springframework.util.StringUtils; -import org.springframework.web.bind.annotation.PathVariable; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestMethod; -import org.springframework.web.bind.annotation.RestController; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Comparator; -import java.util.List; - -@RestController -@RequestMapping("api/scenario") -public class ScenarioController { - - private final ScenarioExecutorService scenarioExecutorService; - private final ScenarioLookupService scenarioLookupService; - private final List scenarios; - - public ScenarioController(ScenarioExecutorService scenarioExecutorService, ScenarioLookupService scenarioLookupService) { - this.scenarioExecutorService = scenarioExecutorService; - this.scenarioLookupService = scenarioLookupService; - this.scenarios = getScenarioList(scenarioLookupService); - } - - private static List getScenarioList(ScenarioLookupService scenarioLookupService) { - final List scenarios = new ArrayList<>(); - scenarioLookupService.getScenarioNames().forEach(name -> scenarios.add(new Scenario(name, Scenario.ScenarioType.MESSAGE_TRIGGERED))); - scenarioLookupService.getStarterNames().forEach(name -> scenarios.add(new Scenario(name, Scenario.ScenarioType.STARTER))); - return scenarios; - } - - /** - * Get a list of scenarios - * - * @param filter - * @return - */ - @RequestMapping(method = RequestMethod.POST) - public Collection getScenarioNames(@RequestBody(required = false) ScenarioFilter filter) { - return scenarios.stream() - .filter(scenario -> { - if (filter != null && StringUtils.hasText(filter.getName())) { - return scenario.name().contains(filter.getName()); - } - return true; - }) - .sorted(Comparator.comparing(Scenario::name)).toList(); - } - - /** - * Get the scenario parameters for the scenario matching the supplied name - * - * @param scenarioName the name of the scenario - * @return the scenario parameters, if any are defined, or an empty list - */ - @RequestMapping(method = RequestMethod.GET, value = "/parameters/{name}") - public Collection getScenarioParameters(@PathVariable("name") String scenarioName) { - return scenarioLookupService.lookupScenarioParameters(scenarioName); - } - - /** - * Launches a scenario using the collection of parameters as scenario variables. This rest service does not - * block until the scenario has completed execution. - * - * @param name - */ - @RequestMapping(method = RequestMethod.POST, value = "/launch/{name}") - public Long launchScenario( - @PathVariable("name") String name, - @RequestBody(required = false) List scenarioParameters) { - return scenarioExecutorService.run(name, scenarioParameters); - } - - public record Scenario(String name, ScenarioController.Scenario.ScenarioType type) { - public enum ScenarioType { - STARTER, - MESSAGE_TRIGGERED - } - - } - - @Data - @NoArgsConstructor - public static class ScenarioFilter { - private String name; - } - -} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/events/ScenariosReloadedEvent.java b/simulator-starter/src/main/java/org/citrusframework/simulator/events/ScenariosReloadedEvent.java new file mode 100644 index 000000000..c2a51a930 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/events/ScenariosReloadedEvent.java @@ -0,0 +1,26 @@ +package org.citrusframework.simulator.events; + +import java.util.Set; +import org.citrusframework.simulator.service.ScenarioLookupService; +import org.springframework.context.ApplicationEvent; + +public final class ScenariosReloadedEvent extends ApplicationEvent { + + private final Set scenarioNames; + private final Set scenarioStarterNames; + + public ScenariosReloadedEvent(ScenarioLookupService source) { + super(source); + + this.scenarioNames = source.getScenarioNames(); + this.scenarioStarterNames = source.getStarterNames(); + } + + public Set getScenarioNames() { + return scenarioNames; + } + + public Set getScenarioStarterNames() { + return scenarioStarterNames; + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java index 7dc8ede24..1aefa78dd 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ActivityService.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2017 the original author or authors. + * Copyright 2006-2023 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. @@ -24,7 +24,6 @@ import org.citrusframework.simulator.model.Message; import org.citrusframework.simulator.model.ScenarioAction; import org.citrusframework.simulator.model.ScenarioExecution; -import org.citrusframework.simulator.model.ScenarioParameter; import org.citrusframework.simulator.repository.ScenarioExecutionRepository; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,29 +57,6 @@ public ActivityService(ScenarioExecutionRepository scenarioExecutionRepository, this.messageService = messageService; } - /** - * Creates a new {@link ScenarioExecution}, persisting it within the database. - * - * @param scenarioName the name of the scenario - * @param scenarioParameters the scenario's start parameters - * @return the new {@link ScenarioExecution} - */ - public ScenarioExecution createExecutionScenario(String scenarioName, Collection scenarioParameters) { - ScenarioExecution scenarioExecution = new ScenarioExecution(); - scenarioExecution.setScenarioName(scenarioName); - scenarioExecution.setStartDate(timeProvider.getTimeNow()); - scenarioExecution.setStatus(ScenarioExecution.Status.RUNNING); - - if (scenarioParameters != null) { - for (ScenarioParameter tp : scenarioParameters) { - scenarioExecution.addScenarioParameter(tp); - } - } - - scenarioExecution = scenarioExecutionRepository.save(scenarioExecution); - return scenarioExecution; - } - public void completeScenarioExecutionSuccess(TestCase testCase) { completeScenarioExecution(ScenarioExecution.Status.SUCCESS, testCase, null); } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderQueryService.java index cf8103b79..e1d0baaca 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderService.java index 6c22286ee..acb05249f 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageHeaderService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.model.Message; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageQueryService.java index f40c18518..7ae672697 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageService.java index 99021022a..2411e2b13 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/MessageService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.model.Message; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java index b1d9631f4..011ff8e51 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/QueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.CriteriaBuilder; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionQueryService.java index 85f9ac6ec..814a8ab67 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionService.java index fdd42834b..566d1be6d 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioActionService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.model.ScenarioAction; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionQueryService.java index a6dab26d7..0b61c1918 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionService.java index 27b884568..474b79abe 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutionService.java @@ -1,6 +1,25 @@ +/* + * Copyright 2023 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; +import jakarta.annotation.Nullable; +import java.util.List; import org.citrusframework.simulator.model.ScenarioExecution; +import org.citrusframework.simulator.model.ScenarioParameter; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -34,4 +53,13 @@ public interface ScenarioExecutionService { * @return the entity. */ Optional findOne(Long id); + + /** + * Creates a new {@link ScenarioExecution}, persisting it within the database. + * + * @param scenarioName the name of the scenario + * @param scenarioParameters the scenario's start parameters + * @return the new {@link ScenarioExecution} + */ + ScenarioExecution createAndSaveExecutionScenario(String scenarioName, @Nullable List scenarioParameters); } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java index 72171bccf..bc224b529 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioExecutorService.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2017 the original author or authors. + * Copyright 2023 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,70 +16,33 @@ package org.citrusframework.simulator.service; -import com.google.common.util.concurrent.ThreadFactoryBuilder; -import org.citrusframework.Citrus; -import org.citrusframework.annotations.CitrusAnnotations; -import org.citrusframework.context.TestContext; -import org.citrusframework.simulator.config.SimulatorConfigurationProperties; -import org.citrusframework.simulator.exception.SimulatorException; -import org.citrusframework.simulator.model.ScenarioExecution; +import jakarta.annotation.Nullable; +import java.util.List; import org.citrusframework.simulator.model.ScenarioParameter; -import org.citrusframework.simulator.scenario.ScenarioRunner; import org.citrusframework.simulator.scenario.SimulatorScenario; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.beans.factory.DisposableBean; -import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextClosedEvent; -import org.springframework.stereotype.Service; -import org.springframework.util.ReflectionUtils; - -import java.util.List; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; -import java.util.concurrent.Future; /** - * Service capable of executing test executables. The service takes care on setting up the executable before execution. Service - * gets a list of normalized parameters which has to be translated to setters on the test executable instance before execution. - * - * @author Christoph Deppisch + * Service capable of executing test executables. The service takes care on setting up the + * executable before execution. Service gets a list of normalized parameters which has to be + * translated to setters on the test executable instance before execution. + *

+ * Careful, this service is not to be confused with the {@link ScenarioExecutionService}. That is + * the "CRUD Service" for {@link org.citrusframework.simulator.model.ScenarioExecution} and has + * nothing to do with actual {@link SimulatorScenario} execution. */ -@Service -public class ScenarioExecutorService implements DisposableBean, ApplicationListener { - - private static final Logger logger = LoggerFactory.getLogger(ScenarioExecutorService.class); - - private final ActivityService activityService; - private final ApplicationContext applicationContext; - private final Citrus citrus; - - private final ExecutorService executorService; - - public ScenarioExecutorService(ActivityService activityService, ApplicationContext applicationContext, Citrus citrus, SimulatorConfigurationProperties properties) { - this.activityService = activityService; - this.applicationContext = applicationContext; - this.citrus = citrus; - - this.executorService = Executors.newFixedThreadPool( - properties.getExecutorThreads(), - new ThreadFactoryBuilder() - .setDaemon(true) - .setNameFormat("execution-svc-thread-%d") - .build() - ); - } +public interface ScenarioExecutorService extends DisposableBean, ApplicationListener { /** * Starts a new scenario instance using the collection of supplied parameters. * * @param name the name of the scenario to start * @param scenarioParameters the list of parameters to pass to the scenario when starting + * @return the scenario execution id */ - public final Long run(String name, List scenarioParameters) { - return run(applicationContext.getBean(name, SimulatorScenario.class), name, scenarioParameters); - } + public Long run(String name, @Nullable List scenarioParameters); /** * Starts a new scenario instance using the collection of supplied parameters. @@ -87,77 +50,7 @@ public final Long run(String name, List scenarioParameters) { * @param scenario the scenario to start * @param name the name of the scenario to start * @param scenarioParameters the list of parameters to pass to the scenario when starting + * @return the scenario execution id */ - public final Long run(SimulatorScenario scenario, String name, List scenarioParameters) { - logger.info(String.format("Starting scenario : %s", name)); - - ScenarioExecution scenarioExecution = activityService.createExecutionScenario(name, scenarioParameters); - - prepareBeforeExecution(scenario); - - startScenarioAsync(scenarioExecution.getExecutionId(), name, scenario, scenarioParameters); - - return scenarioExecution.getExecutionId(); - } - - private Future startScenarioAsync(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { - return executorService.submit(() -> { - try { - TestContext context = citrus.getCitrusContext().createTestContext(); - ReflectionUtils.doWithMethods(scenario.getClass(), method -> { - if (method.getDeclaringClass().equals(SimulatorScenario.class)) { - // no need to execute the default run implementations - return; - } - - if (method.getParameterCount() != 1) { - throw new SimulatorException("Invalid scenario method signature - expect single method parameter but got: " + method.getParameterCount()); - } - - Class parameterType = method.getParameterTypes()[0]; - if (parameterType.equals(ScenarioRunner.class)) { - ScenarioRunner runner = new ScenarioRunner(scenario.getScenarioEndpoint(), applicationContext, context); - if (scenarioParameters != null) { - scenarioParameters.forEach(p -> runner.variable(p.getName(), p.getValue())); - } - - runner.variable(ScenarioExecution.EXECUTION_ID, executionId); - runner.name(String.format("Scenario(%s)", name)); - - CitrusAnnotations.injectAll(scenario, citrus, context); - - try { - runner.start(); - ReflectionUtils.invokeMethod(method, scenario, runner); - } finally { - runner.stop(); - } - } else { - throw new SimulatorException("Invalid scenario method parameter type: " + parameterType); - } - }, method -> method.getName().equals("run")); - logger.debug(String.format("Scenario completed: '%s'", name)); - } catch (Exception e) { - logger.error(String.format("Scenario completed with error: '%s'", name), e); - } - }); - } - - /** - * Prepare scenario instance before execution. Subclasses can add custom preparation steps in here. - * - * @param scenario - */ - protected void prepareBeforeExecution(SimulatorScenario scenario) { - } - - @Override - public void destroy() throws Exception { - executorService.shutdownNow(); - } - - @Override - public void onApplicationEvent(ContextClosedEvent event) { - executorService.shutdownNow(); - } + public Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters); } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioLookupService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioLookupService.java index f0bf23241..3c7ca4503 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioLookupService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioLookupService.java @@ -1,100 +1,60 @@ +/* + * Copyright 2023 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; +import java.util.Collection; +import java.util.Set; import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.scenario.Scenario; import org.citrusframework.simulator.scenario.ScenarioStarter; import org.citrusframework.simulator.scenario.SimulatorScenario; import org.citrusframework.simulator.scenario.Starter; -import org.citrusframework.simulator.scenario.Scenario; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationContext; -import org.springframework.stereotype.Service; - -import jakarta.annotation.PostConstruct; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Map; -import java.util.stream.Collectors; /** - * Service for looking-up and accessing {@link Scenario}s and - * {@link Starter}s + * Service for looking-up and accessing {@link Scenario}'s and {@link Starter}'s. */ -@Service -public class ScenarioLookupService { - private static final Logger LOG = LoggerFactory.getLogger(ScenarioLookupService.class); - - private final ApplicationContext applicationContext; +public interface ScenarioLookupService { /** - * List of available scenario starters + * Reloads the {@link SimulatorScenario} and {@link ScenarioStarter} from the current + * {@link ApplicationContext} */ - private Map scenarioStarters; + void evictAndReloadScenarioCache(); /** - * List of available scenarios - */ - private Map scenarios; - - public ScenarioLookupService(ApplicationContext applicationContext) { - this.applicationContext = applicationContext; - } - - @PostConstruct - private void init() { - scenarios = getSimulatorScenarios(applicationContext); - LOG.info(String.format("Scenarios found: %s", Arrays.toString(scenarios.keySet().toArray()))); - - scenarioStarters = getScenarioStarters(applicationContext); - LOG.info(String.format("Starters found: %s", Arrays.toString(scenarioStarters.keySet().toArray()))); - } - - /** - * Returns the list of parameters that the scenario can be passed when started + * Returns a list containing the names of all scenarios. * - * @param scenarioName - * @return + * @return all scenario names */ - public Collection lookupScenarioParameters(String scenarioName) { - if (scenarioStarters.containsKey(scenarioName)) { - return scenarioStarters.get(scenarioName).getScenarioParameters(); - } - return Collections.emptyList(); - } + Set getScenarioNames(); /** * Returns a list containing the names of all starters * * @return all starter names */ - public Collection getStarterNames() { - return scenarioStarters.keySet().stream() - .sorted() - .toList(); - } + Set getStarterNames(); /** - * Returns a list containing the names of all scenarios. + * Returns the list of parameters that the scenario can be passed when started * - * @return all scenario names + * @param scenarioName the name of the {@link ScenarioStarter} + * @return the {@link ScenarioParameter}'s of the {@link ScenarioStarter} */ - public Collection getScenarioNames() { - return scenarios.keySet().stream() - .sorted() - .toList(); - } - - private static Map getSimulatorScenarios(ApplicationContext context) { - return context.getBeansOfType(SimulatorScenario.class).entrySet().stream() - .filter(map -> !map.getValue().getClass().isAnnotationPresent(Starter.class)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - - } - - private static Map getScenarioStarters(ApplicationContext context) { - return context.getBeansOfType(ScenarioStarter.class).entrySet().stream() - .filter(map -> map.getValue().getClass().isAnnotationPresent(Starter.class)) - .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); - } + Collection lookupScenarioParameters(String scenarioName); } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterQueryService.java index ef871babb..ecfe98663 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterService.java index 1af033a86..9567f6b47 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/ScenarioParameterService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.model.ScenarioParameter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TemplateService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TemplateService.java index 35515086c..b9b344c47 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TemplateService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TemplateService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.config.SimulatorConfigurationProperties; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java index 9a07edd8e..e0d91bd7b 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java index c01f3de90..8461d7a7f 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestParameterService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.model.TestParameter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java index a5d9ea930..7a697ad37 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultQueryService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import jakarta.persistence.criteria.JoinType; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java index 1566895c8..4d4232c97 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TestResultService.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import org.citrusframework.simulator.model.TestResult; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TimeProvider.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TimeProvider.java index 8ec4f82c0..cca7930d1 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/TimeProvider.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/TimeProvider.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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; import java.time.Instant; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java index 7a2e6f25b..aaf8a9ec0 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/Criteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; public interface Criteria { diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageCriteria.java index 816a204c9..26ec283e9 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageHeaderCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageHeaderCriteria.java index 7395f4344..e7cf49760 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageHeaderCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/MessageHeaderCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioActionCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioActionCriteria.java index 36809a82d..b7e48910e 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioActionCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioActionCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioExecutionCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioExecutionCriteria.java index cde8ad49a..8767091de 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioExecutionCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioExecutionCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioParameterCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioParameterCriteria.java index 96a907e44..142f0e066 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioParameterCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/ScenarioParameterCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java index 54dd4639a..74a6e878a 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestParameterCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java index 847f2e4c1..fcfc69541 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/criteria/TestResultCriteria.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.criteria; import org.citrusframework.simulator.service.filter.InstantFilter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java index 73e6794af..214221d8c 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/dto/TestResultByStatus.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.dto; import java.util.Objects; @@ -5,6 +21,6 @@ public record TestResultByStatus(Long successful, Long failed, Long total) { public TestResultByStatus(Long successful, Long failed) { - this(Objects.isNull(successful) ? 0 : successful, Objects.isNull(failed)?0: failed, Objects.isNull(successful) || Objects.isNull(failed) ? 0: successful + failed); + this(Objects.isNull(successful) ? 0 : successful, Objects.isNull(failed) ? 0 : failed, Objects.isNull(successful) || Objects.isNull(failed) ? 0 : successful + failed); } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java index 92c81f36d..c5669eaca 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/Filter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.filter; import java.io.Serializable; @@ -16,6 +32,8 @@ * fieldName.in='something','other' * fieldName.notIn='something','other' * + *

+ * Inspired by JHipster. */ public class Filter implements Serializable { diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java index bb5b0f3f9..bca06609d 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/InstantFilter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.filter; import org.springframework.format.annotation.DateTimeFormat; @@ -7,6 +23,8 @@ /** * Filter class for {@link java.time.Instant} type attributes. + *

+ * Inspired by JHipster. * * @see RangeFilter */ diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java index 2d0e1374b..38f1a2e5f 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/IntegerFilter.java @@ -1,7 +1,25 @@ +/* + * Copyright 2023 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.filter; /** * Filter class for {@link java.lang.Integer} type attributes. + *

+ * Inspired by JHipster. * * @see RangeFilter */ diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java index 579398c33..17174479a 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/LongFilter.java @@ -1,7 +1,25 @@ +/* + * Copyright 2023 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.filter; /** * Filter class for {@link java.lang.Long} type attributes. + *

+ * Inspired by JHipster. * * @see RangeFilter */ diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java index 7c2da9665..3a5b7843e 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/RangeFilter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.filter; import java.util.Objects; @@ -19,6 +35,8 @@ * * Due to problems with the type conversions, the descendant classes should be used, where the generic type parameter * is materialized. + *

+ * Inspired by JHipster. * * @param the type of filter. * @see IntegerFilter diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java index 02393a4f4..9523c9983 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/filter/StringFilter.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.filter; import java.util.Objects; @@ -15,6 +31,8 @@ * fieldName.contains='thing' * fieldName.doesNotContain='thing' * + *

+ * Inspired by JHipster. */ public class StringFilter extends Filter { diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java new file mode 100644 index 000000000..bd828c538 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImpl.java @@ -0,0 +1,168 @@ +/* + * Copyright 2023 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.impl; + +import com.google.common.util.concurrent.ThreadFactoryBuilder; +import jakarta.annotation.Nullable; +import java.lang.reflect.Method; +import java.util.List; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.citrusframework.Citrus; +import org.citrusframework.annotations.CitrusAnnotations; +import org.citrusframework.context.TestContext; +import org.citrusframework.simulator.config.SimulatorConfigurationProperties; +import org.citrusframework.simulator.exception.SimulatorException; +import org.citrusframework.simulator.model.ScenarioExecution; +import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.scenario.ScenarioRunner; +import org.citrusframework.simulator.scenario.SimulatorScenario; +import org.citrusframework.simulator.service.ScenarioExecutionService; +import org.citrusframework.simulator.service.ScenarioExecutorService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationContext; +import org.springframework.context.event.ContextClosedEvent; +import org.springframework.stereotype.Service; +import org.springframework.util.ReflectionUtils; + +/** + * {@inheritDoc} + */ +@Service +public class DefaultScenarioExecutorServiceImpl implements ScenarioExecutorService { + + private static final Logger logger = LoggerFactory.getLogger( DefaultScenarioExecutorServiceImpl.class); + + private final ApplicationContext applicationContext; + private final Citrus citrus; + private final ScenarioExecutionService scenarioExecutionService; + + private final ExecutorService executorService; + + public DefaultScenarioExecutorServiceImpl(ApplicationContext applicationContext, Citrus citrus, ScenarioExecutionService scenarioExecutionService, SimulatorConfigurationProperties properties) { + this.applicationContext = applicationContext; + this.citrus = citrus; + this.scenarioExecutionService = scenarioExecutionService; + + this.executorService = Executors.newFixedThreadPool( + properties.getExecutorThreads(), + new ThreadFactoryBuilder() + .setDaemon(true) + .setNameFormat("execution-svc-thread-%d") + .build() + ); + } + + @Override + public void destroy() throws Exception { + shutdownExecutor(); + } + + @Override + public void onApplicationEvent(ContextClosedEvent event) { + shutdownExecutor(); + } + + @Override + public final Long run(String name, @Nullable List scenarioParameters) { + return run(applicationContext.getBean(name, SimulatorScenario.class), name, scenarioParameters); + } + + @Override + public final Long run(SimulatorScenario scenario, String name, @Nullable List scenarioParameters) { + logger.info("Starting scenario : {}", name); + + ScenarioExecution scenarioExecution = scenarioExecutionService.createAndSaveExecutionScenario(name, scenarioParameters); + + prepareBeforeExecution(scenario); + + startScenarioAsync(scenarioExecution.getExecutionId(), name, scenario, scenarioParameters); + + return scenarioExecution.getExecutionId(); + } + + private Future startScenarioAsync(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { + return executorService.submit(() -> startScenarioSync(executionId, name, scenario, scenarioParameters)); + } + + private void startScenarioSync(Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { + try { + TestContext context = citrus.getCitrusContext().createTestContext(); + ReflectionUtils.doWithMethods( + scenario.getClass(), + method -> createAndRunScenarioRunner(context, method, executionId, name, scenario, scenarioParameters), + method -> method.getName().equals("run") + ); + logger.debug("Scenario completed: {}", name); + } catch (Exception e) { + logger.error("Scenario completed with error: {}!", name, e); + } + } + + private void createAndRunScenarioRunner(TestContext context, Method method, Long executionId, String name, SimulatorScenario scenario, List scenarioParameters) { + if (method.getDeclaringClass().equals(SimulatorScenario.class)) { + // no need to execute the default run implementations + return; + } + + if (method.getParameterCount() != 1) { + throw new SimulatorException("Invalid scenario method signature - expect single method parameter but got: " + method.getParameterCount()); + } + + Class parameterType = method.getParameterTypes()[0]; + if (parameterType.equals(ScenarioRunner.class)) { + ScenarioRunner runner = new ScenarioRunner(scenario.getScenarioEndpoint(), applicationContext, context); + if (scenarioParameters != null) { + scenarioParameters.forEach(p -> runner.variable(p.getName(), p.getValue())); + } + + runner.variable(ScenarioExecution.EXECUTION_ID, executionId); + runner.name(String.format("Scenario(%s)", name)); + + CitrusAnnotations.injectAll(scenario, citrus, context); + + try { + runner.start(); + ReflectionUtils.invokeMethod(method, scenario, runner); + } finally { + runner.stop(); + } + } else { + throw new SimulatorException("Invalid scenario method parameter type: " + parameterType); + } + } + + /** + * Prepare scenario instance before execution. Subclasses can add custom preparation steps in + * here. + * + * @param scenario + */ + protected void prepareBeforeExecution(SimulatorScenario scenario) { + } + + private void shutdownExecutor() { + logger.debug("Request to shutdown executor"); + + if (!executorService.isShutdown()) { + logger.trace("Shutting down executor"); + executorService.shutdownNow(); + } + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageHeaderServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageHeaderServiceImpl.java index 6b21da452..c59616585 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageHeaderServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageHeaderServiceImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.impl; import org.citrusframework.simulator.model.MessageHeader; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageServiceImpl.java index d701bcf5e..7888f86a7 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/MessageServiceImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.impl; import org.citrusframework.simulator.model.Message; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioActionServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioActionServiceImpl.java index 51a1c8404..2f7c605a6 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioActionServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioActionServiceImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.impl; import org.citrusframework.simulator.model.ScenarioAction; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImpl.java index 7a2e32f0b..c8e2c5c02 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImpl.java @@ -1,6 +1,25 @@ +/* + * Copyright 2023 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.impl; +import jakarta.annotation.Nullable; +import java.util.List; import org.citrusframework.simulator.model.ScenarioExecution; +import org.citrusframework.simulator.model.ScenarioParameter; import org.citrusframework.simulator.repository.ScenarioExecutionRepository; import org.citrusframework.simulator.service.ScenarioExecutionService; import org.slf4j.Logger; @@ -11,6 +30,7 @@ import org.springframework.transaction.annotation.Transactional; import java.util.Optional; +import org.springframework.util.CollectionUtils; /** * Service Implementation for managing {@link ScenarioExecution}. @@ -19,7 +39,9 @@ @Transactional public class ScenarioExecutionServiceImpl implements ScenarioExecutionService { - private final Logger log = LoggerFactory.getLogger(ScenarioExecutionServiceImpl.class); + private static final Logger logger = LoggerFactory.getLogger(ScenarioExecutionServiceImpl.class); + + private final TimeProvider timeProvider = new TimeProvider(); private final ScenarioExecutionRepository scenarioExecutionRepository; @@ -29,21 +51,37 @@ public ScenarioExecutionServiceImpl(ScenarioExecutionRepository scenarioExecutio @Override public ScenarioExecution save(ScenarioExecution scenarioExecution) { - log.debug("Request to save ScenarioExecution : {}", scenarioExecution); + logger.debug("Request to save ScenarioExecution : {}", scenarioExecution); return scenarioExecutionRepository.save(scenarioExecution); } @Override @Transactional(readOnly = true) public Page findAll(Pageable pageable) { - log.debug("Request to get all ScenarioExecutions"); + logger.debug("Request to get all ScenarioExecutions"); return scenarioExecutionRepository.findAll(pageable); } @Override @Transactional(readOnly = true) public Optional findOne(Long id) { - log.debug("Request to get ScenarioExecution : {}", id); + logger.debug("Request to get ScenarioExecution : {}", id); return scenarioExecutionRepository.findById(id); } + + @Override + public ScenarioExecution createAndSaveExecutionScenario(String scenarioName, @Nullable List scenarioParameters) { + ScenarioExecution scenarioExecution = new ScenarioExecution(); + scenarioExecution.setScenarioName(scenarioName); + scenarioExecution.setStartDate(timeProvider.getTimeNow()); + scenarioExecution.setStatus(ScenarioExecution.Status.RUNNING); + + if (!CollectionUtils.isEmpty(scenarioParameters)) { + for (ScenarioParameter scenarioParameter : scenarioParameters) { + scenarioExecution.addScenarioParameter(scenarioParameter); + } + } + + return save(scenarioExecution); + } } diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImpl.java new file mode 100644 index 000000000..a62ab58de --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImpl.java @@ -0,0 +1,135 @@ +/* + * Copyright 2023 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.impl; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import org.citrusframework.simulator.events.ScenariosReloadedEvent; +import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.scenario.Scenario; +import org.citrusframework.simulator.scenario.ScenarioStarter; +import org.citrusframework.simulator.scenario.SimulatorScenario; +import org.citrusframework.simulator.scenario.Starter; +import org.citrusframework.simulator.service.ScenarioLookupService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.stereotype.Service; + +/** + * Service for looking-up and accessing {@link Scenario}'s and {@link Starter}'s. + */ +@Service +public class ScenarioLookupServiceImpl implements InitializingBean, ScenarioLookupService { + + private static final Logger logger = LoggerFactory.getLogger(ScenarioLookupServiceImpl.class); + + private final ApplicationContext applicationContext; + private final ApplicationEventPublisher applicationEventPublisher; + + /** + * List of available scenarios + */ + private Map scenarios; + + /** + * List of available scenario starters + */ + private Map scenarioStarters; + + + public ScenarioLookupServiceImpl(ApplicationContext applicationContext, ApplicationEventPublisher applicationEventPublisher) { + this.applicationContext = applicationContext; + this.applicationEventPublisher = applicationEventPublisher; + } + + private static Map getSimulatorScenarios(ApplicationContext context) { + return context.getBeansOfType(SimulatorScenario.class).entrySet().stream() + .filter(map -> !map.getValue().getClass().isAnnotationPresent(Starter.class)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + + } + + private static Map getScenarioStarters(ApplicationContext context) { + return context.getBeansOfType(ScenarioStarter.class).entrySet().stream() + .filter(map -> map.getValue().getClass().isAnnotationPresent(Starter.class)) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public void afterPropertiesSet() { + evictAndReloadScenarioCache(); + + logger.info("Simulator initialized with Scenarios: {}", getScenarioNames()); + logger.info("Simulator initialized with Starters : {}", getStarterNames()); + } + + /** + * Reloads the {@link SimulatorScenario} and {@link ScenarioStarter} from the current {@link ApplicationContext} + */ + @Override + public void evictAndReloadScenarioCache() { + scenarios = getSimulatorScenarios(applicationContext); + logger.debug("Scenarios found: {}", getScenarioNames()); + + scenarioStarters = getScenarioStarters(applicationContext); + logger.debug("Starters found: {}", getStarterNames()); + + applicationEventPublisher.publishEvent(new ScenariosReloadedEvent(this)); + } + + /** + * Returns a list containing the names of all scenarios. + * + * @return all scenario names + */ + @Override + public Set getScenarioNames() { + return scenarios.keySet(); + } + + /** + * Returns a list containing the names of all starters + * + * @return all starter names + */ + @Override + public Set getStarterNames() { + return scenarioStarters.keySet(); + } + + /** + * Returns the list of parameters that the scenario can be passed when started + * + * @param scenarioName the name of the {@link ScenarioStarter} + * @return the {@link ScenarioParameter}'s of the {@link ScenarioStarter} + */ + @Override + public Collection lookupScenarioParameters(String scenarioName) { + if (scenarioStarters.containsKey(scenarioName)) { + return scenarioStarters.get(scenarioName).getScenarioParameters(); + } + + return Collections.emptyList(); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioParameterServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioParameterServiceImpl.java index 0af825355..b02edee69 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioParameterServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/ScenarioParameterServiceImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.impl; import org.citrusframework.simulator.model.ScenarioParameter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java index 35fef1ea5..b86f7b34d 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestParameterServiceImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.impl; import org.citrusframework.simulator.model.TestParameter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java index 7c84544f6..a21b55751 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TestResultServiceImpl.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.impl; import org.citrusframework.simulator.model.TestResult; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TimeProvider.java b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TimeProvider.java new file mode 100644 index 000000000..b8fe5b482 --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/service/impl/TimeProvider.java @@ -0,0 +1,32 @@ +/* + * Copyright 2023 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.impl; + +import java.time.Instant; +import java.time.LocalDateTime; +import java.time.ZoneOffset; + +class TimeProvider { + + TimeProvider() { + // Separate class that allows mocking + } + + Instant getTimeNow() { + return LocalDateTime.now().toInstant(ZoneOffset.UTC); + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/template/TemplateHelper.java b/simulator-starter/src/main/java/org/citrusframework/simulator/template/TemplateHelper.java index 50407acef..3898fda67 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/template/TemplateHelper.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/template/TemplateHelper.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.template; import org.citrusframework.exceptions.CitrusRuntimeException; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageHeaderResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageHeaderResource.java index aef544943..9c67769c6 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageHeaderResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageHeaderResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.MessageHeader; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageResource.java index aa35c47d9..6eac59964 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/MessageResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.Message; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioActionResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioActionResource.java index 3a9e549bd..5d6c868c5 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioActionResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioActionResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.ScenarioAction; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResource.java index 3f7f4b525..5794c8f44 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.ScenarioExecution; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioParameterResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioParameterResource.java index 9c9b0f655..857f74e67 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioParameterResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioParameterResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.ScenarioParameter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioResource.java new file mode 100644 index 000000000..7dbebcc7d --- /dev/null +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/ScenarioResource.java @@ -0,0 +1,128 @@ +/* + * Copyright 2023 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.web.rest; + +import static org.citrusframework.simulator.web.util.PaginationUtil.createPage; + +import jakarta.validation.constraints.NotEmpty; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import org.citrusframework.simulator.events.ScenariosReloadedEvent; +import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.service.ScenarioExecutorService; +import org.citrusframework.simulator.service.ScenarioLookupService; +import org.citrusframework.simulator.web.util.PaginationUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.context.event.EventListener; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.support.ServletUriComponentsBuilder; + +@RestController +@RequestMapping("api") +public class ScenarioResource { + + private static final Logger logger = LoggerFactory.getLogger(ScenarioResource.class); + + private final ScenarioExecutorService scenarioExecutorService; + private final ScenarioLookupService scenarioLookupService; + + private final List scenarioCache = new ArrayList<>(); + + public ScenarioResource(ScenarioExecutorService scenarioExecutorService, ScenarioLookupService scenarioLookupService) { + this.scenarioExecutorService = scenarioExecutorService; + this.scenarioLookupService = scenarioLookupService; + + evictAndReloadScenarioCache(scenarioLookupService.getScenarioNames(), scenarioLookupService.getStarterNames()); + } + + @EventListener(ScenariosReloadedEvent.class) + public void evictAndReloadScenarioCache(ScenariosReloadedEvent scenariosReloadedEvent) { + logger.info("Registered change in scenario cache: {}", scenariosReloadedEvent); + evictAndReloadScenarioCache(scenariosReloadedEvent.getScenarioNames(), scenariosReloadedEvent.getScenarioStarterNames()); + } + + private synchronized void evictAndReloadScenarioCache(Set scenarioNames, Set scenarioStarterNames) { + logger.debug("Scenarios found: {}", scenarioNames); + logger.debug("Starters found: {}", scenarioStarterNames); + + scenarioNames.forEach(name -> scenarioCache.add(new Scenario(name, Scenario.ScenarioType.MESSAGE_TRIGGERED))); + scenarioStarterNames.forEach(name -> scenarioCache.add(new Scenario(name, Scenario.ScenarioType.STARTER))); + + scenarioCache.sort(Comparator.comparing(Scenario::name)); + } + + /** + * Get a list of all registered {@link Scenario}'s in the current simulator + * + * @param pageable the pagination information. + * @return the {@link ResponseEntity} with status {@code 200 (OK)} and the list of scenarios in body. + */ + @GetMapping("/scenarios") + public ResponseEntity> getScenarios(@org.springdoc.core.annotations.ParameterObject Pageable pageable) { + logger.debug("REST request get registered Scenarios"); + + Page page = createPage(scenarioCache, pageable); + HttpHeaders headers = PaginationUtil.generatePaginationHttpHeaders(ServletUriComponentsBuilder.fromCurrentRequest(), page); + return ResponseEntity.ok().headers(headers).body(page.getContent()); + } + + /** + * Get the {@link ScenarioParameter}'s for the {@link Scenario} matching the supplied name + * + * @param scenarioName the name of the scenario + * @return the {@link ScenarioParameter}'s, if any are defined, or an empty list + */ + @GetMapping("/scenarios/{scenarioName}/parameters") + public Collection getScenarioParameters(@PathVariable("scenarioName") String scenarioName) { + logger.debug("REST request get Parameters of Scenario: {}", scenarioName); + return scenarioLookupService.lookupScenarioParameters(scenarioName); + } + + /** + * Launches a {@link Scenario} using the collection of {@link ScenarioParameter} as scenario variables. + *

+ * Note that this rest endpoint does not block until the scenario has completed execution. + * + * @param scenarioName the name of the launched {@link Scenario} + */ + @PostMapping("scenarios/{scenarioName}/launch") + public Long launchScenario(@NotEmpty @PathVariable("scenarioName") String scenarioName, @RequestBody(required = false) List scenarioParameters) { + logger.debug("REST request to launch Scenario '{}' with Parameters: {}", scenarioName, scenarioParameters); + return scenarioExecutorService.run(scenarioName, scenarioParameters); + } + + public record Scenario(String name, ScenarioResource.Scenario.ScenarioType type) { + + public enum ScenarioType { + STARTER, + MESSAGE_TRIGGERED + } + } +} diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java index 564d26b6f..56a07d0d7 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestParameterResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.TestParameter; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java index 97072ca28..b55ae7bb4 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/rest/TestResultResource.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.rest; import org.citrusframework.simulator.model.TestResult; diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java index a12ec24e9..be2182657 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/PaginationUtil.java @@ -1,6 +1,26 @@ +/* + * Copyright 2023 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.web.util; +import java.util.Collection; +import java.util.List; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; import org.springframework.http.HttpHeaders; import org.springframework.web.util.UriComponentsBuilder; @@ -9,7 +29,6 @@ /** * Utility class for handling pagination. - * *

* Pagination uses the same principles as the GitHub API, * and follow RFC 5988 (Link header). @@ -19,6 +38,14 @@ public interface PaginationUtil { String HEADER_X_TOTAL_COUNT = "X-Total-Count"; String HEADER_LINK_FORMAT = "<{0}>; rel=\"{1}\""; + static Page createPage(List objects, Pageable pageable) { + int start = (int) pageable.getOffset(); + int end = Math.min((start + pageable.getPageSize()), objects.size()); + + List pageContent = objects.subList(start, end); + return new PageImpl<>(pageContent, pageable, objects.size()); + } + /** * Generate pagination headers for a Spring Data {@link org.springframework.data.domain.Page} object. * diff --git a/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java index 3ed02565c..91a58e008 100644 --- a/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java +++ b/simulator-starter/src/main/java/org/citrusframework/simulator/web/util/ResponseUtil.java @@ -1,3 +1,19 @@ +/* + * Copyright 2023 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.web.util; import org.springframework.http.HttpHeaders; diff --git a/simulator-starter/src/main/resources/static/favicon.ico b/simulator-starter/src/main/resources/static/favicon.ico index 8bb33660f42851c436efee2a571db4d701c58167..aab06acef708976716322e2cc3af05a17ad5af7c 100644 GIT binary patch literal 15406 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 H&Vl~{X=p)l literal 16958 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 T8b6uQ_zNe_(epH$-s%4VL}@Vx diff --git a/simulator-starter/src/main/resources/static/index.html b/simulator-starter/src/main/resources/static/index.html index 759446bc0..82ea828e4 100644 --- a/simulator-starter/src/main/resources/static/index.html +++ b/simulator-starter/src/main/resources/static/index.html @@ -13,17 +13,15 @@

Citrus Simulator

Welcome to the standalone Citrus simulator application!

- -

Summary

-

Active

-

Activity

-

Scenarios

-

Messages

+ +

Summary

+

Scenarios

+

Messages


https://citrusframework.org
-
Sponsored by ConSol Software GmbH
+
Open Source on GitHub
diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventTest.java new file mode 100644 index 000000000..42618300c --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/events/ScenariosReloadedEventTest.java @@ -0,0 +1,38 @@ +package org.citrusframework.simulator.events; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; + +import java.util.Set; +import org.citrusframework.simulator.service.ScenarioLookupService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class ScenariosReloadedEventTest { + + private static final Set MOCK_SCENARIO_NAMES = Set.of("Scenario1", "Scenario2"); + private static final Set MOCK_STARTER_NAMES = Set.of("Starter1", "Starter2"); + + @Mock + private ScenarioLookupService scenarioLookupServiceMock; + + private ScenariosReloadedEvent fixture; + + @BeforeEach + void beforeEachSetup() { + doReturn(MOCK_SCENARIO_NAMES).when(scenarioLookupServiceMock).getScenarioNames(); + doReturn(MOCK_STARTER_NAMES).when(scenarioLookupServiceMock).getStarterNames(); + + fixture = new ScenariosReloadedEvent(scenarioLookupServiceMock); + } + + @Test + void extractInformationFromSource() { + assertEquals(MOCK_SCENARIO_NAMES, fixture.getScenarioNames()); + assertEquals(MOCK_STARTER_NAMES, fixture.getScenarioStarterNames()); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java new file mode 100644 index 000000000..b62ffa161 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/DefaultScenarioExecutorServiceImplTest.java @@ -0,0 +1,185 @@ +package org.citrusframework.simulator.service.impl; + +// Import statements + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; + +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.atomic.AtomicBoolean; +import org.apache.commons.lang3.NotImplementedException; +import org.citrusframework.Citrus; +import org.citrusframework.CitrusContext; +import org.citrusframework.TestCase; +import org.citrusframework.context.TestContext; +import org.citrusframework.report.TestListeners; +import org.citrusframework.simulator.config.SimulatorConfigurationProperties; +import org.citrusframework.simulator.model.ScenarioExecution; +import org.citrusframework.simulator.model.ScenarioParameter; +import org.citrusframework.simulator.scenario.ScenarioEndpoint; +import org.citrusframework.simulator.scenario.ScenarioRunner; +import org.citrusframework.simulator.scenario.SimulatorScenario; +import org.citrusframework.simulator.service.ScenarioExecutionService; +import org.junit.jupiter.api.BeforeEach; +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 org.springframework.context.event.ContextClosedEvent; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class DefaultScenarioExecutorServiceImplTest { + + private static ScenarioEndpoint scenarioEndpointMock; + private static AtomicBoolean customScenarioExecuted; + + @Mock + private ApplicationContext applicationContextMock; + + @Mock + private Citrus citrusMock; + + @Mock + private ScenarioExecutionService scenarioExecutionServiceMock; + + @Mock + private SimulatorConfigurationProperties propertiesMock; + + @Mock + private ExecutorService executorServiceMock; + + private DefaultScenarioExecutorServiceImpl fixture; + + private String scenarioName = "testScenario"; + private List parameters = List.of( + ScenarioParameter.builder() + .name("param1") + .value("value1") + .build(), + ScenarioParameter.builder() + .name("param2") + .value("value2") + .build() + ); + + @BeforeEach + public void beforeEachSetup() { + doReturn(1).when(propertiesMock).getExecutorThreads(); + + fixture = new DefaultScenarioExecutorServiceImpl(applicationContextMock, citrusMock, scenarioExecutionServiceMock,propertiesMock ); + ReflectionTestUtils.setField(fixture, "executorService", executorServiceMock, ExecutorService.class); + + scenarioEndpointMock = mock(ScenarioEndpoint.class); + customScenarioExecuted = new AtomicBoolean(false); + } + + @Test + void runSimulatorScenarioByName() { + SimulatorScenario simulatorScenarioMock = mock(SimulatorScenario.class); + doReturn(simulatorScenarioMock).when(applicationContextMock).getBean(scenarioName, SimulatorScenario.class); + + Long executionId = mockScenarioExecutionCreation(); + + // Note that this does not actually "run" the scenario (because of the mocked executor service), it just creates it + Long result = fixture.run(scenarioName, parameters); + assertEquals(executionId, result); + + ArgumentCaptor scenarioRunnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(executorServiceMock).submit(scenarioRunnableArgumentCaptor.capture()); + + // Now, we need more mocks! + mockCitrusTestContext(); + + // This invokes the scenario execution with the captured runnable + scenarioRunnableArgumentCaptor.getValue().run(); + + verifyNoInteractions(simulatorScenarioMock); + } + + @Test + void runScenarioDirectly() { + Long executionId = mockScenarioExecutionCreation(); + + SimulatorScenario simulatorScenario = spy(new CustomSimulatorScenario()); + + // 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); + assertEquals(executionId, result); + + ArgumentCaptor scenarioRunnableArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(executorServiceMock).submit(scenarioRunnableArgumentCaptor.capture()); + + // Now, we need more mocks! + TestContext testContextMock = mockCitrusTestContext(); + TestListeners testListenersMock = mock(TestListeners.class); + doReturn(testListenersMock).when(testContextMock).getTestListeners(); + + // This invokes the scenario execution with the captured runnable + scenarioRunnableArgumentCaptor.getValue().run(); + + ArgumentCaptor scenarioRunnerArgumentCaptor = ArgumentCaptor.forClass(ScenarioRunner.class); + verify(simulatorScenario).run(scenarioRunnerArgumentCaptor.capture()); + + ScenarioRunner scenarioRunner = scenarioRunnerArgumentCaptor.getValue(); + assertEquals(scenarioEndpointMock, scenarioRunner.scenarioEndpoint()); + verify(testListenersMock).onTestStart(any(TestCase.class)); + verify(testListenersMock).onTestSuccess(any(TestCase.class)); + verify(testListenersMock).onTestFinish(any(TestCase.class)); + verifyNoMoreInteractions(testListenersMock); + + assertTrue(customScenarioExecuted.get()); + } + + @Test + void shutdownExecutorOnDestroy() throws Exception { + fixture.destroy(); + verify(executorServiceMock).shutdownNow(); + } + + @Test + void shutdownExecutorOnApplicationContextEvent() { + fixture.onApplicationEvent(new ContextClosedEvent(applicationContextMock)); + verify(executorServiceMock).shutdownNow(); + } + + private Long mockScenarioExecutionCreation() { + Long executionId = 1L; + ScenarioExecution scenarioExecutionMock = mock(ScenarioExecution.class); + doReturn(executionId).when(scenarioExecutionMock).getExecutionId(); + doReturn(scenarioExecutionMock).when(scenarioExecutionServiceMock).createAndSaveExecutionScenario(scenarioName, parameters); + return executionId; + } + + private TestContext mockCitrusTestContext() { + CitrusContext citrusContextMock = mock(CitrusContext.class); + doReturn(citrusContextMock).when(citrusMock).getCitrusContext(); + TestContext testContextMock = mock(TestContext.class); + doReturn(testContextMock).when(citrusContextMock).createTestContext(); + return testContextMock; + } + + public static class CustomSimulatorScenario implements SimulatorScenario { + + @Override + public ScenarioEndpoint getScenarioEndpoint() { + return scenarioEndpointMock; + } + + @Override + public void run(ScenarioRunner runner) { + customScenarioExecuted.set(true); + } + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImplTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImplTest.java index efaacfb53..e2997aefe 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImplTest.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioExecutionServiceImplTest.java @@ -1,6 +1,7 @@ package org.citrusframework.simulator.service.impl; import org.citrusframework.simulator.model.ScenarioExecution; +import org.citrusframework.simulator.model.ScenarioParameter; import org.citrusframework.simulator.repository.ScenarioExecutionRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -10,13 +11,18 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; +import org.springframework.test.util.ReflectionTestUtils; +import java.time.Instant; import java.util.List; import java.util.Optional; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.when; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doReturn; @ExtendWith(MockitoExtension.class) class ScenarioExecutionServiceImplTest { @@ -24,6 +30,9 @@ class ScenarioExecutionServiceImplTest { @Mock private ScenarioExecutionRepository scenarioExecutionRepositoryMock; + @Mock + TimeProvider timeProviderMock; + private ScenarioExecution sampleScenarioExecution; private ScenarioExecutionServiceImpl fixture; @@ -33,11 +42,12 @@ void beforeEachSetup() { sampleScenarioExecution = new ScenarioExecution(); fixture = new ScenarioExecutionServiceImpl(scenarioExecutionRepositoryMock); + ReflectionTestUtils.setField(fixture, "timeProvider", timeProviderMock, TimeProvider.class); } @Test void testSave() { - when(scenarioExecutionRepositoryMock.save(sampleScenarioExecution)).thenReturn(sampleScenarioExecution); + doReturn(sampleScenarioExecution).when(scenarioExecutionRepositoryMock).save(sampleScenarioExecution); ScenarioExecution savedScenarioExecution = fixture.save(sampleScenarioExecution); assertEquals(sampleScenarioExecution, savedScenarioExecution); @@ -48,7 +58,7 @@ void testFindAll() { Pageable pageable = Pageable.unpaged(); Page page = new PageImpl<>(List.of(sampleScenarioExecution)); - when(scenarioExecutionRepositoryMock.findAll(pageable)).thenReturn(page); + doReturn(page).when(scenarioExecutionRepositoryMock).findAll(pageable); Page result = fixture.findAll(pageable); @@ -59,11 +69,33 @@ void testFindAll() { void testFindOne() { Long scenarioExecutionId = 1L; - when(scenarioExecutionRepositoryMock.findById(scenarioExecutionId)).thenReturn(Optional.of(sampleScenarioExecution)); + doReturn(Optional.of(sampleScenarioExecution)).when(scenarioExecutionRepositoryMock).findById(scenarioExecutionId); Optional maybeScenarioExecution = fixture.findOne(scenarioExecutionId); assertTrue(maybeScenarioExecution.isPresent()); assertEquals(sampleScenarioExecution, maybeScenarioExecution.get()); } + + @Test + void testCreateAndSaveExecutionScenario() { + String scenarioName = "sampleScenario"; + + Instant now = Instant.now(); + doReturn(now).when(timeProviderMock).getTimeNow(); + + doAnswer(invocationOnMock -> invocationOnMock.getArgument(0, ScenarioExecution.class)).when(scenarioExecutionRepositoryMock).save(any(ScenarioExecution.class)); + + ScenarioParameter scenarioParameter = new ScenarioParameter(); + List scenarioParameters = List.of(scenarioParameter); + + ScenarioExecution result = fixture.createAndSaveExecutionScenario(scenarioName, scenarioParameters); + + assertEquals(scenarioName, result.getScenarioName()); + assertEquals(now, result.getStartDate()); + assertEquals(ScenarioExecution.Status.RUNNING, result.getStatus()); + assertThat(result.getScenarioParameters()) + .hasSize(1) + .containsExactly(scenarioParameter); + } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java new file mode 100644 index 000000000..c2555db23 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/service/impl/ScenarioLookupServiceImplTest.java @@ -0,0 +1,167 @@ +package org.citrusframework.simulator.service.impl; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Stream; +import org.apache.commons.lang3.NotImplementedException; +import org.citrusframework.simulator.events.ScenariosReloadedEvent; +import org.citrusframework.simulator.model.ScenarioParameter; +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; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationEventPublisher; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class ScenarioLookupServiceImplTest { + + private static final ScenarioParameter SCENARIO_PARAMETER = ScenarioParameter.builder() + .name("parameter-name") + .value("parameter-value") + .build(); + + @Mock + private ApplicationContext applicationContextMock; + + @Mock + private ApplicationEventPublisher applicationEventPublisherMock; + + private ScenarioLookupServiceImpl fixture; + + @BeforeEach + void beforeEachSetup() { + fixture = new ScenarioLookupServiceImpl(applicationContextMock, + applicationEventPublisherMock); + } + + static Stream evictAndReloadScenarioCache() { + return Stream.of( + Arguments.of( + (Consumer) ScenarioLookupServiceImpl::afterPropertiesSet), + Arguments.of( + (Consumer) ScenarioLookupServiceImpl::evictAndReloadScenarioCache) + ); + } + + @MethodSource + @ParameterizedTest + void evictAndReloadScenarioCache(Consumer invocation) { + final String testSimulatorScenario = "testSimulatorScenario"; + Map contextSimulatorScenarios = Map.of(testSimulatorScenario, new TestSimulatorScenario(), "invalidTestSimulatorScenario", new InvalidTestSimulatorScenario()); + doReturn(contextSimulatorScenarios).when(applicationContextMock).getBeansOfType(SimulatorScenario.class); + + final String testScenarioStarter = "testScenarioStarter"; + Map contextScenarioStarters = Map.of(testScenarioStarter, new TetsScenarioStarter()); + doReturn(contextScenarioStarters).when(applicationContextMock).getBeansOfType(ScenarioStarter.class); + + invocation.accept(fixture); + + Map simulatorScenarios = (Map) ReflectionTestUtils.getField(fixture, "scenarios"); + assertThat(simulatorScenarios).hasSize(1); + + Map scenarioStarters = (Map) ReflectionTestUtils.getField(fixture, "scenarioStarters"); + assertThat(scenarioStarters).hasSize(1); + + ArgumentCaptor scenariosReloadedEventArgumentCaptor = ArgumentCaptor.forClass(ScenariosReloadedEvent.class); + verify(applicationEventPublisherMock).publishEvent(scenariosReloadedEventArgumentCaptor.capture()); + + ScenariosReloadedEvent scenariosReloadedEvent = scenariosReloadedEventArgumentCaptor.getValue(); + assertThat(scenariosReloadedEvent.getScenarioNames()).containsExactly(testSimulatorScenario); + assertThat(scenariosReloadedEvent.getScenarioStarterNames()).containsExactly(testScenarioStarter); + } + + @Test + void getScenarioNames() { + final String testSimulatorScenario = "testSimulatorScenario"; + ReflectionTestUtils.setField(fixture, "scenarios", Map.of(testSimulatorScenario, new TestSimulatorScenario()), Map.class); + + assertThat(fixture.getScenarioNames()) + .hasSize(1) + .containsExactly(testSimulatorScenario); + } + + @Test + void getStarterNames() { + final String testScenarioStarter = "testScenarioStarter"; + ReflectionTestUtils.setField(fixture, "scenarioStarters", Map.of(testScenarioStarter, new TetsScenarioStarter()), Map.class); + + assertThat(fixture.getStarterNames()) + .hasSize(1) + .containsExactly(testScenarioStarter); + } + + @Test + void lookupScenarioParameters() { + String scenarioName = "scenarioName"; + + Map scenarioStartersMock = Map.of(scenarioName, new TetsScenarioStarter()); + ReflectionTestUtils.setField(fixture, "scenarioStarters", scenarioStartersMock, Map.class); + + assertThat(fixture.lookupScenarioParameters(scenarioName)) + .hasSize(1) + .containsExactly(SCENARIO_PARAMETER); + } + + @Test + void lookupScenarioParametersReturnsEmptyListForInvalidScenarioNames() { + String scenarioName = "scenarioName"; + + Map scenarioStartersMock = mock(Map.class); + ReflectionTestUtils.setField(fixture, "scenarioStarters", scenarioStartersMock, Map.class); + + doReturn(false).when(scenarioStartersMock).containsKey(scenarioName); + + assertThat(fixture.lookupScenarioParameters(scenarioName)) + .isEmpty(); + } + + @Scenario("testSimulatorScenario") + private static class TestSimulatorScenario implements SimulatorScenario { + + @Override + public ScenarioEndpoint getScenarioEndpoint() { + throw new NotImplementedException(); + } + } + + @Starter("testScenarioStarter") + private static class TetsScenarioStarter implements ScenarioStarter { + + @Override + public List getScenarioParameters() { + return List.of(SCENARIO_PARAMETER); + } + + @Override + public ScenarioEndpoint getScenarioEndpoint() { + throw new NotImplementedException(); + } + } + + @Starter("invalidTestScenarioStarter") + private static class InvalidTestSimulatorScenario implements SimulatorScenario { + + @Override + public ScenarioEndpoint getScenarioEndpoint() { + throw new NotImplementedException(); + } + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageHeaderResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageHeaderResourceIT.java index 91370cea1..eb125addc 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageHeaderResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageHeaderResourceIT.java @@ -59,7 +59,7 @@ public class MessageHeaderResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restMessageHeaderMockMvc; + private MockMvc mockMvc; private MessageHeader messageHeader; @@ -127,7 +127,7 @@ void getAllMessageHeaders() throws Exception { messageHeaderRepository.saveAndFlush(messageHeader); // Get all the messageHeaderList - restMessageHeaderMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=headerId,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -145,7 +145,7 @@ void getMessageHeader() throws Exception { messageHeaderRepository.saveAndFlush(messageHeader); // Get the messageHeader - restMessageHeaderMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, messageHeader.getHeaderId())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -512,7 +512,7 @@ void getAllMessageHeadersByMessageIsEqualToSomething() throws Exception { * Executes the search, and checks that the default entity is returned. */ private void defaultMessageHeaderShouldBeFound(String filter) throws Exception { - restMessageHeaderMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=headerId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -523,7 +523,7 @@ private void defaultMessageHeaderShouldBeFound(String filter) throws Exception { .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); // Check, that the count call also returns 1 - restMessageHeaderMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=headerId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -534,7 +534,7 @@ private void defaultMessageHeaderShouldBeFound(String filter) throws Exception { * Executes the search, and checks that the default entity is not returned. */ private void defaultMessageHeaderShouldNotBeFound(String filter) throws Exception { - restMessageHeaderMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=headerId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -542,7 +542,7 @@ private void defaultMessageHeaderShouldNotBeFound(String filter) throws Exceptio .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restMessageHeaderMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=headerId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -553,6 +553,6 @@ private void defaultMessageHeaderShouldNotBeFound(String filter) throws Exceptio @Transactional void getNonExistingMessageHeader() throws Exception { // Get the messageHeader - restMessageHeaderMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageResourceIT.java index 721e676ce..8a835a0b4 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/MessageResourceIT.java @@ -60,7 +60,7 @@ public class MessageResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restMessageMockMvc; + private MockMvc mockMvc; private Message message; @@ -108,7 +108,7 @@ void getAllMessages() throws Exception { messageRepository.saveAndFlush(message); // Get all the messageList - restMessageMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=messageId,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -127,7 +127,7 @@ void getMessage() throws Exception { messageRepository.saveAndFlush(message); // Get the message - restMessageMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, message.getMessageId())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -556,7 +556,7 @@ void getAllMessagesByLastModifiedDateIsGreaterThanSomething() throws Exception { * Executes the search, and checks that the default entity is returned. */ private void defaultMessageShouldBeFound(String filter) throws Exception { - restMessageMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=messageId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -568,7 +568,7 @@ private void defaultMessageShouldBeFound(String filter) throws Exception { .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); // Check, that the count call also returns 1 - restMessageMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=messageId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -579,7 +579,7 @@ private void defaultMessageShouldBeFound(String filter) throws Exception { * Executes the search, and checks that the default entity is not returned. */ private void defaultMessageShouldNotBeFound(String filter) throws Exception { - restMessageMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=messageId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -587,7 +587,7 @@ private void defaultMessageShouldNotBeFound(String filter) throws Exception { .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restMessageMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=messageId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -598,6 +598,6 @@ private void defaultMessageShouldNotBeFound(String filter) throws Exception { @Transactional void getNonExistingMessage() throws Exception { // Get the message - restMessageMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioActionResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioActionResourceIT.java index 6f8b1c67c..3f5a456d8 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioActionResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioActionResourceIT.java @@ -55,7 +55,7 @@ public class ScenarioActionResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restScenarioActionMockMvc; + private MockMvc mockMvc; private ScenarioAction scenarioAction; @@ -99,7 +99,7 @@ void getAllScenarioActions() throws Exception { scenarioActionRepository.saveAndFlush(scenarioAction); // Get all the scenarioActionList - restScenarioActionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=actionId,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -116,7 +116,7 @@ void getScenarioAction() throws Exception { scenarioActionRepository.saveAndFlush(scenarioAction); // Get the scenarioAction - restScenarioActionMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, scenarioAction.getActionId())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -365,7 +365,7 @@ void getAllScenarioActionsByScenarioExecutionIsEqualToSomething() throws Excepti * Executes the search, and checks that the default entity is returned. */ private void defaultScenarioActionShouldBeFound(String filter) throws Exception { - restScenarioActionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=actionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -375,7 +375,7 @@ private void defaultScenarioActionShouldBeFound(String filter) throws Exception .andExpect(jsonPath("$.[*].endDate").value(hasItem(DEFAULT_END_DATE.toString()))); // Check, that the count call also returns 1 - restScenarioActionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=actionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -386,7 +386,7 @@ private void defaultScenarioActionShouldBeFound(String filter) throws Exception * Executes the search, and checks that the default entity is not returned. */ private void defaultScenarioActionShouldNotBeFound(String filter) throws Exception { - restScenarioActionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=actionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -394,7 +394,7 @@ private void defaultScenarioActionShouldNotBeFound(String filter) throws Excepti .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restScenarioActionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=actionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -405,6 +405,6 @@ private void defaultScenarioActionShouldNotBeFound(String filter) throws Excepti @Transactional void getNonExistingScenarioAction() throws Exception { // Get the scenarioAction - restScenarioActionMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResourceIT.java index 595d3f5f9..8bb610fd6 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioExecutionResourceIT.java @@ -58,7 +58,7 @@ public class ScenarioExecutionResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restScenarioExecutionMockMvc; + private MockMvc mockMvc; private ScenarioExecution scenarioExecution; @@ -108,7 +108,7 @@ void getAllScenarioExecutions() throws Exception { scenarioExecutionRepository.saveAndFlush(scenarioExecution); // Get all the scenarioExecutionList - restScenarioExecutionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=executionId,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -127,7 +127,7 @@ void getScenarioExecution() throws Exception { scenarioExecutionRepository.saveAndFlush(scenarioExecution); // Get the scenarioExecution - restScenarioExecutionMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, scenarioExecution.getExecutionId())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -526,7 +526,7 @@ void getAllScenarioExecutionsByScenarioParametersIsEqualToSomething() throws Exc * Executes the search, and checks that the default entity is returned. */ private void defaultScenarioExecutionShouldBeFound(String filter) throws Exception { - restScenarioExecutionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=executionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -538,7 +538,7 @@ private void defaultScenarioExecutionShouldBeFound(String filter) throws Excepti .andExpect(jsonPath("$.[*].errorMessage").value(hasItem(DEFAULT_ERROR_MESSAGE))); // Check, that the count call also returns 1 - restScenarioExecutionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=executionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -549,7 +549,7 @@ private void defaultScenarioExecutionShouldBeFound(String filter) throws Excepti * Executes the search, and checks that the default entity is not returned. */ private void defaultScenarioExecutionShouldNotBeFound(String filter) throws Exception { - restScenarioExecutionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=executionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -557,7 +557,7 @@ private void defaultScenarioExecutionShouldNotBeFound(String filter) throws Exce .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restScenarioExecutionMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=executionId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -568,6 +568,6 @@ private void defaultScenarioExecutionShouldNotBeFound(String filter) throws Exce @Transactional void getNonExistingScenarioExecution() throws Exception { // Get the scenarioExecution - restScenarioExecutionMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioParameterResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioParameterResourceIT.java index 6e57f85c6..e0b02ca74 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioParameterResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioParameterResourceIT.java @@ -59,7 +59,7 @@ public class ScenarioParameterResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restScenarioParameterMockMvc; + private MockMvc mockMvc; private ScenarioParameter scenarioParameter; @@ -107,7 +107,7 @@ void getAllScenarioParameters() throws Exception { scenarioParameterRepository.saveAndFlush(scenarioParameter); // Get all the scenarioParameterList - restScenarioParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=parameterId,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -126,7 +126,7 @@ void getScenarioParameter() throws Exception { scenarioParameterRepository.saveAndFlush(scenarioParameter); // Get the scenarioParameter - restScenarioParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, scenarioParameter.getParameterId())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -520,7 +520,7 @@ void getAllScenarioParametersByScenarioExecutionIsEqualToSomething() throws Exce * Executes the search, and checks that the default entity is returned. */ private void defaultScenarioParameterShouldBeFound(String filter) throws Exception { - restScenarioParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=parameterId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -532,7 +532,7 @@ private void defaultScenarioParameterShouldBeFound(String filter) throws Excepti .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); // Check, that the count call also returns 1 - restScenarioParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=parameterId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -543,7 +543,7 @@ private void defaultScenarioParameterShouldBeFound(String filter) throws Excepti * Executes the search, and checks that the default entity is not returned. */ private void defaultScenarioParameterShouldNotBeFound(String filter) throws Exception { - restScenarioParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=parameterId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -551,7 +551,7 @@ private void defaultScenarioParameterShouldNotBeFound(String filter) throws Exce .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restScenarioParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=parameterId,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -562,6 +562,6 @@ private void defaultScenarioParameterShouldNotBeFound(String filter) throws Exce @Transactional void getNonExistingScenarioParameter() throws Exception { // Get the scenarioParameter - restScenarioParameterMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java new file mode 100644 index 000000000..80af151b6 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceIT.java @@ -0,0 +1,73 @@ +package org.citrusframework.simulator.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.citrusframework.simulator.web.rest.TestUtil.sameInstant; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.hasItem; +import static org.mockito.Mockito.doReturn; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import java.util.List; +import java.util.Set; +import org.citrusframework.simulator.IntegrationTest; +import org.citrusframework.simulator.events.ScenariosReloadedEvent; +import org.citrusframework.simulator.service.ScenarioExecutorService; +import org.citrusframework.simulator.service.ScenarioLookupService; +import org.citrusframework.simulator.web.rest.ScenarioResource.Scenario; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.http.MediaType; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.transaction.annotation.Transactional; + +/** + * Integration tests for the {@link ScenarioResource} REST controller. + *

+ * The scanned scenarios and starters come + * from {@link org.citrusframework.simulator.service.impl.ScenarioLookupServiceImplTest}. + */ +@IntegrationTest +@AutoConfigureMockMvc +class ScenarioResourceIT { + + private static final String ENTITY_API_URL = "/api/scenarios"; + private static final String ENTITY_API_URL_SCENARIO_NAME = ENTITY_API_URL + "/{scenarioName}"; + + @Autowired + private MockMvc restScenarioParameterMockMvc; + + @Test + void getAllScenarioNames() throws Exception { + // Get all the scenarioParameterList + restScenarioParameterMockMvc + .perform(get(ENTITY_API_URL )) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.length()").value(equalTo(2))) + .andExpect(jsonPath("$.[0].name").value(equalTo("testScenarioStarter"))) + .andExpect(jsonPath("$.[0].type").value(equalTo("STARTER"))) + .andExpect(jsonPath("$.[1].name").value(equalTo("testSimulatorScenario"))) + .andExpect(jsonPath("$.[1].type").value(equalTo("MESSAGE_TRIGGERED"))); + } + + @Test + void getAllScenarioStarterParameters() throws Exception { + // Get all the scenarioParameterList + restScenarioParameterMockMvc + .perform(get(ENTITY_API_URL_SCENARIO_NAME+"/parameters", "testScenarioStarter" )) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) + .andExpect(jsonPath("$.length()").value(equalTo(1))) + .andExpect(jsonPath("$.[0].name").value(equalTo("parameter-name"))) + .andExpect(jsonPath("$.[0].value").value(equalTo("parameter-value"))); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceTest.java new file mode 100644 index 000000000..212f0da11 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/ScenarioResourceTest.java @@ -0,0 +1,50 @@ +package org.citrusframework.simulator.web.rest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.doReturn; + +import java.util.List; +import java.util.Set; +import org.citrusframework.simulator.events.ScenariosReloadedEvent; +import org.citrusframework.simulator.service.ScenarioExecutorService; +import org.citrusframework.simulator.service.ScenarioLookupService; +import org.citrusframework.simulator.web.rest.ScenarioResource.Scenario; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.test.util.ReflectionTestUtils; + +@ExtendWith(MockitoExtension.class) +class ScenarioResourceTest { + + @Mock + private ScenarioExecutorService scenarioExecutorServiceMock; + + @Mock + private ScenarioLookupService scenarioLookupServiceMock; + + private ScenarioResource fixture; + + @BeforeEach + void beforeEachSetup() { + fixture = new ScenarioResource(scenarioExecutorServiceMock, scenarioLookupServiceMock); + } + + @Test + void evictAndReloadScenarioCache() { + Set mockScenarioNames = Set.of("Scenario2", "Scenario1"); + Set mockStarterNames = Set.of("Starter2", "Starter1"); + + doReturn(mockScenarioNames).when(scenarioLookupServiceMock).getScenarioNames(); + doReturn(mockStarterNames).when(scenarioLookupServiceMock).getStarterNames(); + + fixture.evictAndReloadScenarioCache(new ScenariosReloadedEvent(scenarioLookupServiceMock)); + + assertThat((List) ReflectionTestUtils.getField(fixture, "scenarioCache")) + .hasSize(4) + .extracting("name") + .containsExactly("Scenario1", "Scenario2", "Starter1", "Starter2"); + } +} diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java index 297cb8cf7..a35e5cd8d 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestParameterResourceIT.java @@ -56,7 +56,7 @@ class TestParameterResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restTestParameterMockMvc; + private MockMvc mockMvc; private TestParameter testParameter; @@ -124,7 +124,7 @@ void getAllTestParameters() throws Exception { testParameterRepository.saveAndFlush(testParameter); // Get all the testParameterList - restTestParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=createdDate,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -141,7 +141,7 @@ void getTestParameter() throws Exception { testParameterRepository.saveAndFlush(testParameter); // Get the testParameter - restTestParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, testParameter.getTestResult().getId(), testParameter.getKey())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -489,7 +489,7 @@ void getAllTestParametersByTestResultIsEqualToSomething() throws Exception { * Executes the search, and checks that the default entity is returned. */ private void defaultTestParameterShouldBeFound(String filter) throws Exception { - restTestParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=createdDate,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -499,7 +499,7 @@ private void defaultTestParameterShouldBeFound(String filter) throws Exception { .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); // Check, that the count call also returns 1 - restTestParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=createdDate,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -510,7 +510,7 @@ private void defaultTestParameterShouldBeFound(String filter) throws Exception { * Executes the search, and checks that the default entity is not returned. */ private void defaultTestParameterShouldNotBeFound(String filter) throws Exception { - restTestParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=createdDate,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -518,7 +518,7 @@ private void defaultTestParameterShouldNotBeFound(String filter) throws Exceptio .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restTestParameterMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=createdDate,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -529,6 +529,6 @@ private void defaultTestParameterShouldNotBeFound(String filter) throws Exceptio @Transactional void getNonExistingTestParameter() throws Exception { // Get the testParameter - restTestParameterMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE, "non-existing-key")).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE, "non-existing-key")).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java index 7a17e9f17..187551b38 100644 --- a/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/rest/TestResultResourceIT.java @@ -68,7 +68,7 @@ class TestResultResourceIT { private EntityManager entityManager; @Autowired - private MockMvc restTestResultMockMvc; + private MockMvc mockMvc; private TestResult testResult; @@ -122,7 +122,7 @@ void getAllTestResults() throws Exception { testResultRepository.saveAndFlush(testResult); // Get all the testResultList - restTestResultMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=id,desc")) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -144,7 +144,7 @@ void getTestResult() throws Exception { testResultRepository.saveAndFlush(testResult); // Get the testResult - restTestResultMockMvc + mockMvc .perform(get(ENTITY_API_URL_ID, testResult.getId())) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -749,7 +749,7 @@ void getAllTestResultsByTestParameterIsEqualToSomething() throws Exception { * Executes the search, and checks that the default entity is returned. */ private void defaultTestResultShouldBeFound(String filter) throws Exception { - restTestResultMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=id,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -764,7 +764,7 @@ private void defaultTestResultShouldBeFound(String filter) throws Exception { .andExpect(jsonPath("$.[*].lastModifiedDate").value(hasItem(sameInstant(DEFAULT_LAST_MODIFIED_DATE)))); // Check, that the count call also returns 1 - restTestResultMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=id,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -775,7 +775,7 @@ private void defaultTestResultShouldBeFound(String filter) throws Exception { * Executes the search, and checks that the default entity is not returned. */ private void defaultTestResultShouldNotBeFound(String filter) throws Exception { - restTestResultMockMvc + mockMvc .perform(get(ENTITY_API_URL + "?sort=id,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -783,7 +783,7 @@ private void defaultTestResultShouldNotBeFound(String filter) throws Exception { .andExpect(jsonPath("$").isEmpty()); // Check, that the count call also returns 0 - restTestResultMockMvc + mockMvc .perform(get(ENTITY_API_URL + "/count?sort=id,desc&" + filter)) .andExpect(status().isOk()) .andExpect(content().contentType(MediaType.APPLICATION_JSON_VALUE)) @@ -794,6 +794,6 @@ private void defaultTestResultShouldNotBeFound(String filter) throws Exception { @Transactional void getNonExistingTestResult() throws Exception { // Get the testResult - restTestResultMockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); + mockMvc.perform(get(ENTITY_API_URL_ID, Long.MAX_VALUE)).andExpect(status().isNotFound()); } } diff --git a/simulator-starter/src/test/java/org/citrusframework/simulator/web/util/PaginationUtilTest.java b/simulator-starter/src/test/java/org/citrusframework/simulator/web/util/PaginationUtilTest.java new file mode 100644 index 000000000..9c2be8dc4 --- /dev/null +++ b/simulator-starter/src/test/java/org/citrusframework/simulator/web/util/PaginationUtilTest.java @@ -0,0 +1,37 @@ +package org.citrusframework.simulator.web.util; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; + +class PaginationUtilTest { + + @Test + void testCreatePage() { + // Prepare test data + List allObjects = Arrays.asList("Object1", "Object2", "Object3", "Object4", "Object5"); + Pageable pageable = PageRequest.of(1, 2); // Requesting the second page with a size of 2 + + Pageable mockPageable = mock(Pageable.class); + doReturn(2L).when(mockPageable).getOffset(); + doReturn(2).when(mockPageable).getPageSize(); + doReturn(pageable).when(mockPageable).previousOrFirst(); + + // Call the method to test + Page page = PaginationUtil.createPage(allObjects, mockPageable); + + // Assertions + assertEquals(2, page.getContent().size(), "The page content size should be 2"); + assertEquals("Object3", page.getContent().get(0), "First object on the second page"); + assertEquals("Object4", page.getContent().get(1), "Second object on the second page"); + assertEquals(5, page.getTotalElements(), "Total elements should be 5"); + assertEquals(3, page.getTotalPages(), "Total pages should be 3"); + } +} diff --git a/simulator-ui/.jhipster/Scenario.json b/simulator-ui/.jhipster/Scenario.json new file mode 100644 index 000000000..595f0831c --- /dev/null +++ b/simulator-ui/.jhipster/Scenario.json @@ -0,0 +1,32 @@ +{ + "changelogDate": "20231026180542", + "dto": "no", + "entityTableName": "scenario", + "fields": [ + { + "fieldName": "name", + "fieldType": "String", + "fieldValidateRules": ["required"] + }, + { + "fieldName": "type", + "fieldType": "Integer", + "fieldValidateRules": ["required"] + } + ], + "jpaMetamodelFiltering": true, + "name": "Scenario", + "pagination": "pagination", + "readOnly": true, + "relationships": [ + { + "otherEntityName": "scenarioParameter", + "otherEntityRelationshipName": "scenario", + "relationshipName": "scenarioParameters", + "relationshipSide": "left", + "relationshipType": "one-to-many" + } + ], + "searchEngine": "no", + "service": "serviceImpl" +} diff --git a/simulator-ui/.yo-rc.json b/simulator-ui/.yo-rc.json index eb46c33f6..4ea62067d 100644 --- a/simulator-ui/.yo-rc.json +++ b/simulator-ui/.yo-rc.json @@ -8,7 +8,16 @@ "creationTimestamp": 1694794967405, "devServerPort": 4200, "enableTranslation": true, - "entities": ["Message", "MessageHeader", "ScenarioExecution", "ScenarioAction", "ScenarioParameter", "TestParameter", "TestResult"], + "entities": [ + "Message", + "MessageHeader", + "Scenario", + "ScenarioExecution", + "ScenarioAction", + "ScenarioParameter", + "TestParameter", + "TestResult" + ], "jhipsterVersion": "8.0.0-beta.3", "languages": ["en"], "microfrontend": false, diff --git a/simulator-ui/src/main/webapp/app/app-routing.module.ts b/simulator-ui/src/main/webapp/app/app-routing.module.ts index ae48e3af2..c485b3f8b 100644 --- a/simulator-ui/src/main/webapp/app/app-routing.module.ts +++ b/simulator-ui/src/main/webapp/app/app-routing.module.ts @@ -25,6 +25,11 @@ import NavbarComponent from './layouts/navbar/navbar.component'; path: '', loadChildren: () => import(`./entities/entity-routing.module`).then(({ EntityRoutingModule }) => EntityRoutingModule), }, + { + path: 'scenario', + data: { pageTitle: 'citrusSimulatorApp.scenario.home.title' }, + loadChildren: () => import(`./scenario/scenario.routes`), + }, ...errorRoute, ], { enableTracing: DEBUG_INFO_ENABLED, bindToComponentInputs: true }, diff --git a/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts b/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts index 2d445905b..6e5f94ebc 100644 --- a/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts +++ b/simulator-ui/src/main/webapp/app/config/font-awesome-icons.ts @@ -5,6 +5,7 @@ import { faBars, faBell, faBook, + faBoxOpen, faCalendarAlt, faChartPie, faCheck, @@ -13,11 +14,14 @@ import { faDatabase, faEye, faFlag, + faHeading, faHeart, faHome, faList, faLock, + faMessage, faPencilAlt, + faPlay, faPlus, faRoad, faSave, @@ -50,6 +54,7 @@ export const fontAwesomeIcons = [ faBars, faBell, faBook, + faBoxOpen, faCalendarAlt, faChartPie, faCheck, @@ -59,11 +64,14 @@ export const fontAwesomeIcons = [ faEye, faFlag, faGithub, + faHeading, faHeart, faHome, faList, faLock, + faMessage, faPencilAlt, + faPlay, faPlus, faRoad, faSave, diff --git a/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header.component.ts b/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header.component.ts index 5156e31e7..23666575c 100644 --- a/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header.component.ts +++ b/simulator-ui/src/main/webapp/app/entities/message-header/list/message-header.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, NgZone, OnInit } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; import { combineLatest, Observable, switchMap, tap } from 'rxjs'; @@ -45,6 +45,7 @@ export class MessageHeaderComponent implements OnInit { page = 1; constructor( + private ngZone: NgZone, protected messageHeaderService: MessageHeaderService, protected activatedRoute: ActivatedRoute, public router: Router, @@ -135,10 +136,12 @@ export class MessageHeaderComponent implements OnInit { queryParamsObj[filterOption.nameAsQueryParam()] = filterOption.values; }); - this.router.navigate(['./'], { - relativeTo: this.activatedRoute, - queryParams: queryParamsObj, - }); + this.ngZone.run(() => + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); } protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { diff --git a/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html b/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html index 89513153d..5243d6944 100644 --- a/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html +++ b/simulator-ui/src/main/webapp/app/entities/message/list/message.component.html @@ -93,7 +93,7 @@

class="btn btn-info btn-sm" data-cy="filterOtherEntityButton" > - + + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); } protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { diff --git a/simulator-ui/src/main/webapp/app/entities/message/message.model.ts b/simulator-ui/src/main/webapp/app/entities/message/message.model.ts index 425fc0537..b733fc5b2 100644 --- a/simulator-ui/src/main/webapp/app/entities/message/message.model.ts +++ b/simulator-ui/src/main/webapp/app/entities/message/message.model.ts @@ -4,7 +4,7 @@ import { IScenarioExecution } from 'app/entities/scenario-execution/scenario-exe export interface IMessage { messageId: number; - direction?: number | null; + direction?: 'UNKNOWN' | 'INBOUND' | 'OUTBOUND' | null; payload?: string | null; citrusMessageId?: string | null; scenarioExecutionId?: number | null; diff --git a/simulator-ui/src/main/webapp/app/entities/message/message.test-samples.ts b/simulator-ui/src/main/webapp/app/entities/message/message.test-samples.ts index e428b0d3f..e71b8015c 100644 --- a/simulator-ui/src/main/webapp/app/entities/message/message.test-samples.ts +++ b/simulator-ui/src/main/webapp/app/entities/message/message.test-samples.ts @@ -4,7 +4,7 @@ import { IMessage, NewMessage } from './message.model'; export const sampleWithRequiredData: IMessage = { messageId: 4989, - direction: 26770, + direction: 'INBOUND', citrusMessageId: 'fooey incidentally whether', createdDate: dayjs('2023-10-13T01:53'), lastModifiedDate: dayjs('2023-10-13T12:44'), @@ -12,7 +12,7 @@ export const sampleWithRequiredData: IMessage = { export const sampleWithPartialData: IMessage = { messageId: 11846, - direction: 8779, + direction: 'OUTBOUND', citrusMessageId: 'ouch', createdDate: dayjs('2023-10-12T16:06'), lastModifiedDate: dayjs('2023-10-12T23:13'), @@ -20,7 +20,7 @@ export const sampleWithPartialData: IMessage = { export const sampleWithFullData: IMessage = { messageId: 6858, - direction: 4539, + direction: 'INBOUND', payload: 'yuck bite spectacular', citrusMessageId: 'bah who', createdDate: dayjs('2023-10-12T18:05'), @@ -28,7 +28,7 @@ export const sampleWithFullData: IMessage = { }; export const sampleWithNewData: NewMessage = { - direction: 6174, + direction: 'UNKNOWN', citrusMessageId: 'cheap put', createdDate: dayjs('2023-10-12T23:44'), lastModifiedDate: dayjs('2023-10-13T00:54'), diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-action/list/scenario-action.component.ts b/simulator-ui/src/main/webapp/app/entities/scenario-action/list/scenario-action.component.ts index 56ceb6c1c..b929f408d 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-action/list/scenario-action.component.ts +++ b/simulator-ui/src/main/webapp/app/entities/scenario-action/list/scenario-action.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, NgZone, OnInit } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; import { combineLatest, Observable, switchMap, tap } from 'rxjs'; @@ -45,6 +45,7 @@ export class ScenarioActionComponent implements OnInit { page = 1; constructor( + private ngZone: NgZone, protected scenarioActionService: ScenarioActionService, protected activatedRoute: ActivatedRoute, public router: Router, @@ -135,10 +136,12 @@ export class ScenarioActionComponent implements OnInit { queryParamsObj[filterOption.nameAsQueryParam()] = filterOption.values; }); - this.router.navigate(['./'], { - relativeTo: this.activatedRoute, - queryParams: queryParamsObj, - }); + this.ngZone.run(() => + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); } protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { 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 a3ac3c88a..59a5bae6d 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 @@ -42,15 +42,15 @@

- +
- Status + Scenario Name
- +
- Scenario Name + Status
@@ -82,7 +82,7 @@

class="btn btn-info btn-sm" data-cy="filterOtherEntityButton" > - + class="btn btn-info btn-sm" data-cy="filterOtherEntityButton" > - + class="btn btn-info btn-sm" data-cy="filterOtherEntityButton" > - + + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); } protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.model.ts b/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.model.ts index aa2aa809e..7dc9b62c6 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.model.ts +++ b/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.model.ts @@ -5,7 +5,7 @@ export interface IScenarioExecution { startDate?: dayjs.Dayjs | null; endDate?: dayjs.Dayjs | null; scenarioName?: string | null; - status?: number | null; + status?: 'UNKNOWN' | 'RUNNING' | 'SUCCESS' | 'FAILED' | null; errorMessage?: string | null; } diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.test-samples.ts b/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.test-samples.ts index 3495311bc..44ebf3785 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.test-samples.ts +++ b/simulator-ui/src/main/webapp/app/entities/scenario-execution/scenario-execution.test-samples.ts @@ -6,14 +6,14 @@ export const sampleWithRequiredData: IScenarioExecution = { executionId: 28068, startDate: dayjs('2023-10-18T19:26'), scenarioName: 'geez', - status: 31399, + status: 'RUNNING', }; export const sampleWithPartialData: IScenarioExecution = { executionId: 6290, startDate: dayjs('2023-10-18T20:56'), scenarioName: 'baptise gracefully', - status: 4003, + status: 'SUCCESS', errorMessage: 'gee sniffle bunch', }; @@ -22,14 +22,14 @@ export const sampleWithFullData: IScenarioExecution = { startDate: dayjs('2023-10-19T02:53'), endDate: dayjs('2023-10-18T17:11'), scenarioName: 'midst', - status: 18914, + status: 'FAILED', errorMessage: 'jiffy wherever', }; export const sampleWithNewData: NewScenarioExecution = { startDate: dayjs('2023-10-18T16:49'), scenarioName: 'robotics', - status: 1073, + status: 'UNKNOWN', executionId: null, }; diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.ts b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.ts index f3a035026..2cd5027f4 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.ts +++ b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/list/scenario-parameter.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, NgZone, OnInit } from '@angular/core'; import { HttpHeaders } from '@angular/common/http'; import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; import { combineLatest, Observable, switchMap, tap } from 'rxjs'; @@ -45,6 +45,7 @@ export class ScenarioParameterComponent implements OnInit { page = 1; constructor( + private ngZone: NgZone, protected scenarioParameterService: ScenarioParameterService, protected activatedRoute: ActivatedRoute, public router: Router, @@ -135,10 +136,12 @@ export class ScenarioParameterComponent implements OnInit { queryParamsObj[filterOption.nameAsQueryParam()] = filterOption.values; }); - this.router.navigate(['./'], { - relativeTo: this.activatedRoute, - queryParams: queryParamsObj, - }); + this.ngZone.run(() => + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); } protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.model.ts b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.model.ts index 4ce49f39e..06db100e3 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.model.ts +++ b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.model.ts @@ -3,7 +3,7 @@ import { IScenarioExecution } from 'app/entities/scenario-execution/scenario-exe export interface IScenarioParameter { parameterId: number; - name?: string | null; + name?: 'UNKNOWN' | 'TEXTBOX' | 'TEXTAREA' | 'DROPDOWN' | null; controlType?: number | null; value?: string | null; createdDate?: number | null; diff --git a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.test-samples.ts b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.test-samples.ts index a4c314ff2..dbd867ad6 100644 --- a/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.test-samples.ts +++ b/simulator-ui/src/main/webapp/app/entities/scenario-parameter/scenario-parameter.test-samples.ts @@ -4,7 +4,7 @@ import { IScenarioParameter, NewScenarioParameter } from './scenario-parameter.m export const sampleWithRequiredData: IScenarioParameter = { parameterId: 13246, - name: 'separately', + name: 'TEXTBOX', controlType: 26023, value: 'underexpose since commerce', createdDate: 21593, @@ -13,7 +13,7 @@ export const sampleWithRequiredData: IScenarioParameter = { export const sampleWithPartialData: IScenarioParameter = { parameterId: 25616, - name: 'jellyfish adolescent on', + name: 'TEXTAREA', controlType: 3663, value: 'outside hastily', createdDate: 26077, @@ -22,7 +22,7 @@ export const sampleWithPartialData: IScenarioParameter = { export const sampleWithFullData: IScenarioParameter = { parameterId: 16729, - name: 'cytokine', + name: 'DROPDOWN', controlType: 21298, value: 'coarse', createdDate: 9490, @@ -30,7 +30,7 @@ export const sampleWithFullData: IScenarioParameter = { }; export const sampleWithNewData: NewScenarioParameter = { - name: 'who', + name: 'UNKNOWN', controlType: 24095, value: 'whoa', createdDate: 11500, diff --git a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html index 9741b3ee4..775b02f64 100644 --- a/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html +++ b/simulator-ui/src/main/webapp/app/entities/test-result/list/test-result.component.html @@ -103,12 +103,12 @@

class="btn btn-info btn-sm" data-cy="filterOtherEntityButton" > - + Show Test ParameterShow Test Parameters + + + diff --git a/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.spec.ts b/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.spec.ts new file mode 100644 index 000000000..314a3c2d2 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.spec.ts @@ -0,0 +1,52 @@ +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { ActivatedRoute, provideRouter, withComponentInputBinding } from '@angular/router'; +import { RouterTestingHarness, RouterTestingModule } from '@angular/router/testing'; + +import { of } from 'rxjs'; + +import { IScenarioParameter } from 'app/entities/scenario-parameter/scenario-parameter.model'; + +import { ScenarioDetailComponent } from './scenario-detail.component'; + +const scenarioParameters: IScenarioParameter[] = [{ parameterId: 123 }]; + +describe('Scenario Management Detail Component', () => { + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, ScenarioDetailComponent, RouterTestingModule.withRoutes([], { bindToComponentInputs: true })], + providers: [ + provideRouter( + [ + { + path: '**', + component: ScenarioDetailComponent, + resolve: { scenarioParameters: () => of(scenarioParameters) }, + }, + ], + withComponentInputBinding(), + ), + { + provide: ActivatedRoute, + useValue: { + queryParamMap: of(jest.requireActual('@angular/router').convertToParamMap({ name: 'test-scenario', type: 'STARTER' })), + }, + }, + ], + }) + .overrideTemplate(ScenarioDetailComponent, '') + .compileComponents(); + }); + + describe('OnInit', () => { + it('Should load scenario on init', async () => { + const harness = await RouterTestingHarness.create(); + const instance = await harness.navigateByUrl('/', ScenarioDetailComponent); + + // THEN + // expect(instance.name).toEqual('test-scenario'); + // expect(instance.type).toEqual('STARTER'); + expect(instance.scenarioParameters).toEqual(scenarioParameters); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.ts b/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.ts new file mode 100644 index 000000000..7aad7ba82 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/detail/scenario-detail.component.ts @@ -0,0 +1,31 @@ +import { Component, Input } from '@angular/core'; +import { ActivatedRoute, RouterModule } from '@angular/router'; + +import { IScenarioParameter } from 'app/entities/scenario-parameter/scenario-parameter.model'; +import { ScenarioParameterService } from 'app/entities/scenario-parameter/service/scenario-parameter.service'; +import SharedModule from 'app/shared/shared.module'; +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; + +@Component({ + standalone: true, + selector: 'jhi-scenario-detail', + templateUrl: './scenario-detail.component.html', + imports: [SharedModule, RouterModule, DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe], +}) +export class ScenarioDetailComponent { + @Input() name: string | null = null; + @Input() type: 'STARTER' | 'MESSAGE_TRIGGERED' | null = null; + + @Input() scenarioParameters: IScenarioParameter[] | null = null; + + constructor( + protected activatedRoute: ActivatedRoute, + private scenarioParameterService: ScenarioParameterService, + ) {} + + trackId = (_index: number, item: IScenarioParameter): number => this.scenarioParameterService.getScenarioParameterIdentifier(item); + + previousState(): void { + window.history.back(); + } +} diff --git a/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html new file mode 100644 index 000000000..ae8dc50d5 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.html @@ -0,0 +1,85 @@ +
+

+ Scenarios + +
+ +
+

+ + + + + +
+ No Scenarios found +
+ +
+ + + + + + + + + + + + + + + +
+
+ Name + +
+
+
+ Type + +
+
+ {{ scenario.name }} + {{ scenario.type }} +
+ + +
+
+
+ +
+
+ +
+ +
+ +
+
+
diff --git a/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.spec.ts b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.spec.ts new file mode 100644 index 000000000..77d4e04c5 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.spec.ts @@ -0,0 +1,130 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { HttpHeaders, HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { of } from 'rxjs'; + +import { TranslateModule } from '@ngx-translate/core'; + +import { ScenarioService } from '../service/scenario.service'; + +import { ScenarioComponent } from './scenario.component'; + +import SpyInstance = jest.SpyInstance; + +describe('Scenario Management Component', () => { + let comp: ScenarioComponent; + let fixture: ComponentFixture; + let service: ScenarioService; + let routerNavigateSpy: SpyInstance>; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + RouterTestingModule.withRoutes([{ path: 'scenario', component: ScenarioComponent }]), + HttpClientTestingModule, + ScenarioComponent, + TranslateModule.forRoot(), + ], + providers: [ + { + provide: ActivatedRoute, + useValue: { + data: of({ + defaultSort: 'id,asc', + }), + queryParamMap: of( + jest.requireActual('@angular/router').convertToParamMap({ + page: '1', + size: '1', + sort: 'id,desc', + 'filter[someId.in]': 'dc4279ea-cfb9-11ec-9d64-0242ac120002', + }), + ), + snapshot: { queryParams: {} }, + }, + }, + ], + }) + .overrideTemplate(ScenarioComponent, '') + .compileComponents(); + + fixture = TestBed.createComponent(ScenarioComponent); + comp = fixture.componentInstance; + service = TestBed.inject(ScenarioService); + routerNavigateSpy = jest.spyOn(comp.router, 'navigate'); + + const headers = new HttpHeaders(); + jest.spyOn(service, 'query').mockReturnValue( + of( + new HttpResponse({ + body: [{ name: 'test-scenario' }], + headers, + }), + ), + ); + }); + + it('Should call load all on init', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenCalled(); + expect(comp.scenarios?.[0]).toEqual(expect.objectContaining({ name: 'test-scenario' })); + }); + + describe('trackId', () => { + it('Should forward to scenarioService', () => { + const entity = { name: 'test-scenario' }; + jest.spyOn(service, 'getScenarioIdentifier'); + const name = comp.trackId(0, entity); + expect(service.getScenarioIdentifier).toHaveBeenCalledWith(entity); + expect(name).toBe(entity.name); + }); + }); + + it('should load a page', () => { + // WHEN + comp.navigateToPage(1); + + // THEN + expect(routerNavigateSpy).toHaveBeenCalled(); + }); + + it('should calculate the sort attribute for an id', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith(expect.objectContaining({ sort: ['id,desc'] })); + }); + + it('should calculate the sort attribute for a non-id attribute', () => { + // GIVEN + comp.predicate = 'name'; + + // WHEN + comp.navigateToWithComponentValues(); + + // THEN + expect(routerNavigateSpy).toHaveBeenLastCalledWith( + expect.anything(), + expect.objectContaining({ + queryParams: expect.objectContaining({ + sort: ['name,asc'], + }), + }), + ); + }); + + it('should calculate no filter attribute', () => { + // WHEN + comp.ngOnInit(); + + // THEN + expect(service.query).toHaveBeenLastCalledWith({ page: 0, size: 20, sort: ['id,desc'] }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.ts b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.ts new file mode 100644 index 000000000..f514a941a --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/list/scenario.component.ts @@ -0,0 +1,171 @@ +import { Component, NgZone, OnInit } from '@angular/core'; +import { HttpHeaders } from '@angular/common/http'; +import { FormsModule } from '@angular/forms'; +import { ActivatedRoute, Data, ParamMap, Router, RouterModule } from '@angular/router'; + +import { combineLatest, Observable, switchMap, tap } from 'rxjs'; + +import { ITEMS_PER_PAGE, PAGE_HEADER, TOTAL_COUNT_RESPONSE_HEADER } from 'app/config/pagination.constants'; +import { ASC, DESC, SORT, DEFAULT_SORT_DATA } from 'app/config/navigation.constants'; + +import { DurationPipe, FormatMediumDatetimePipe, FormatMediumDatePipe } from 'app/shared/date'; +import { AlertService } from 'app/core/util/alert.service'; +import { FilterComponent, IFilterOption } from 'app/shared/filter'; +import { ItemCountComponent } from 'app/shared/pagination'; +import SharedModule from 'app/shared/shared.module'; +import { SortDirective, SortByDirective } from 'app/shared/sort'; + +import { EntityArrayResponseType, ScenarioService } from '../service/scenario.service'; +import { IScenario } from '../scenario.model'; +import { filter, map } from 'rxjs/operators'; + +@Component({ + standalone: true, + selector: 'jhi-scenario', + templateUrl: './scenario.component.html', + imports: [ + RouterModule, + FormsModule, + SharedModule, + SortDirective, + SortByDirective, + DurationPipe, + FormatMediumDatetimePipe, + FormatMediumDatePipe, + FilterComponent, + ItemCountComponent, + ], +}) +export class ScenarioComponent implements OnInit { + scenarios?: IScenario[]; + isLoading = false; + + predicate = 'id'; + ascending = true; + + itemsPerPage = ITEMS_PER_PAGE; + totalItems = 0; + page = 1; + + constructor( + private alertService: AlertService, + private ngZone: NgZone, + protected scenarioService: ScenarioService, + protected activatedRoute: ActivatedRoute, + public router: Router, + ) {} + + trackId = (_index: number, item: IScenario): string => this.scenarioService.getScenarioIdentifier(item); + + ngOnInit(): void { + this.load(); + } + + load(): void { + this.loadFromBackendWithRouteInformations().subscribe({ + next: (res: EntityArrayResponseType) => { + this.onResponseSuccess(res); + }, + }); + } + + navigateToWithComponentValues(): void { + this.handleNavigation(this.page, this.predicate, this.ascending); + } + + navigateToPage(page = this.page): void { + this.handleNavigation(page, this.predicate, this.ascending); + } + + protected loadFromBackendWithRouteInformations(): Observable { + return combineLatest([this.activatedRoute.queryParamMap, this.activatedRoute.data]).pipe( + tap(([params, data]) => this.fillComponentAttributeFromRoute(params, data)), + switchMap(() => this.queryBackend(this.page, this.predicate, this.ascending)), + ); + } + + protected fillComponentAttributeFromRoute(params: ParamMap, data: Data): void { + const page = params.get(PAGE_HEADER); + this.page = +(page ?? 1); + const sort = (params.get(SORT) ?? data[DEFAULT_SORT_DATA]).split(','); + this.predicate = sort[0]; + this.ascending = sort[1] === ASC; + } + + protected onResponseSuccess(response: EntityArrayResponseType): void { + this.fillComponentAttributesFromResponseHeader(response.headers); + const dataFromBody = this.fillComponentAttributesFromResponseBody(response.body); + this.scenarios = dataFromBody; + } + + protected fillComponentAttributesFromResponseBody(data: IScenario[] | null): IScenario[] { + return data ?? []; + } + + protected fillComponentAttributesFromResponseHeader(headers: HttpHeaders): void { + this.totalItems = Number(headers.get(TOTAL_COUNT_RESPONSE_HEADER)); + } + + protected queryBackend(page?: number, predicate?: string, ascending?: boolean): Observable { + this.isLoading = true; + const pageToLoad: number = page ?? 1; + const queryObject: any = { + page: pageToLoad - 1, + size: this.itemsPerPage, + sort: this.getSortQueryParam(predicate, ascending), + }; + return this.scenarioService.query(queryObject).pipe(tap(() => (this.isLoading = false))); + } + + protected handleNavigation(page = this.page, predicate?: string, ascending?: boolean, filterOptions?: IFilterOption[]): void { + const queryParamsObj: any = { + page, + size: this.itemsPerPage, + sort: this.getSortQueryParam(predicate, ascending), + }; + + filterOptions?.forEach(filterOption => { + queryParamsObj[filterOption.nameAsQueryParam()] = filterOption.values; + }); + + this.ngZone.run(() => + this.router.navigate(['./'], { + relativeTo: this.activatedRoute, + queryParams: queryParamsObj, + }), + ); + } + + protected getSortQueryParam(predicate = this.predicate, ascending = this.ascending): string[] { + const ascendingQueryParam = ascending ? ASC : DESC; + if (predicate === '') { + return []; + } else { + return [predicate + ',' + ascendingQueryParam]; + } + } + + protected launch(scenario: IScenario): void { + this.scenarioService + .launch(scenario.name) + .pipe( + filter(response => !!response.body), + map(response => response.body), + ) + .subscribe({ + next: scenarioExecutionId => { + this.alertService.addAlert({ + type: 'success', + translationKey: 'citrusSimulatorApp.scenario.action.launchedSuccessfully', + translationParams: { scenarioExecutionId }, + }); + }, + error: () => { + this.alertService.addAlert({ + type: 'success', + translationKey: 'citrusSimulatorApp.scenario.action.launchFailed', + }); + }, + }); + } +} diff --git a/simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.spec.ts b/simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.spec.ts new file mode 100644 index 000000000..754eac021 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.spec.ts @@ -0,0 +1,101 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpResponse } from '@angular/common/http'; +import { HttpClientTestingModule } from '@angular/common/http/testing'; +import { ActivatedRouteSnapshot, ActivatedRoute, Router, convertToParamMap } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; + +import { of } from 'rxjs'; + +import { IScenarioParameter } from 'app/entities/scenario-parameter/scenario-parameter.model'; + +import { ScenarioService } from '../service/scenario.service'; + +import scenarioParameterResolve from './scenario-parameter-routing-resolve.service'; + +describe('ScenarioParameter routing resolve service', () => { + let mockRouter: Router; + let mockActivatedRouteSnapshot: ActivatedRouteSnapshot; + let service: ScenarioService; + let resultParameters: IScenarioParameter[] | null | undefined; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule, RouterTestingModule.withRoutes([])], + providers: [ + { + provide: ActivatedRoute, + useValue: { + snapshot: { + paramMap: convertToParamMap({}), + }, + }, + }, + ], + }); + mockRouter = TestBed.inject(Router); + jest.spyOn(mockRouter, 'navigate').mockImplementation(() => Promise.resolve(true)); + mockActivatedRouteSnapshot = TestBed.inject(ActivatedRoute).snapshot; + service = TestBed.inject(ScenarioService); + resultParameters = undefined; + }); + + describe('resolve', () => { + it('should return IScenarioParameters returned by findParameters', () => { + // GIVEN + service.findParameters = jest.fn(name => of(new HttpResponse({ body: [{ parameterId: 123 }] }))); + mockActivatedRouteSnapshot.params = { name: 'test-scenario' }; + + // WHEN + TestBed.runInInjectionContext(() => { + scenarioParameterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultParameters = result; + }, + }); + }); + + // THEN + expect(service.findParameters).toBeCalledWith('test-scenario'); + expect(resultParameters).toEqual([{ parameterId: 123 }]); + }); + + it('should return null if name is not provided', () => { + // GIVEN + service.findParameters = jest.fn(); + mockActivatedRouteSnapshot.params = {}; + + // WHEN + TestBed.runInInjectionContext(() => { + scenarioParameterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultParameters = result; + }, + }); + }); + + // THEN + expect(service.findParameters).not.toBeCalled(); + expect(resultParameters).toEqual(null); + }); + + it('should route to 404 page if data not found in server', () => { + // GIVEN + jest.spyOn(service, 'findParameters').mockReturnValue(of(new HttpResponse({ body: null }))); + mockActivatedRouteSnapshot.params = { name: 'test-scenario' }; + + // WHEN + TestBed.runInInjectionContext(() => { + scenarioParameterResolve(mockActivatedRouteSnapshot).subscribe({ + next(result) { + resultParameters = result; + }, + }); + }); + + // THEN + expect(service.findParameters).toBeCalledWith('test-scenario'); + expect(resultParameters).toEqual(undefined); + expect(mockRouter.navigate).toHaveBeenCalledWith(['404']); + }); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.ts b/simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.ts new file mode 100644 index 000000000..403435e88 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/route/scenario-parameter-routing-resolve.service.ts @@ -0,0 +1,33 @@ +import { inject } from '@angular/core'; +import { HttpResponse } from '@angular/common/http'; +import { ActivatedRouteSnapshot, Router } from '@angular/router'; +import { of, EMPTY, Observable } from 'rxjs'; +import { map, mergeMap } from 'rxjs/operators'; + +import { IScenario } from '../scenario.model'; +import { ScenarioService } from '../service/scenario.service'; +import { IScenarioParameter } from '../../entities/scenario-parameter/scenario-parameter.model'; + +const scenarioParameterByNameComparator = (a: IScenarioParameter, b: IScenarioParameter): number => a.name!.localeCompare(b.name!); + +export const scenarioParameterResolve = (route: ActivatedRouteSnapshot): Observable => { + const name = route.params['name']; + if (name) { + return inject(ScenarioService) + .findParameters(name) + .pipe( + mergeMap((scenario: HttpResponse) => { + if (scenario.body) { + return of(scenario.body); + } else { + inject(Router).navigate(['404']); + return EMPTY; + } + }), + map(scenarioParameters => scenarioParameters.sort(scenarioParameterByNameComparator)), + ); + } + return of(null); +}; + +export default scenarioParameterResolve; diff --git a/simulator-ui/src/main/webapp/app/scenario/scenario.model.ts b/simulator-ui/src/main/webapp/app/scenario/scenario.model.ts new file mode 100644 index 000000000..7f7730935 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/scenario.model.ts @@ -0,0 +1,4 @@ +export interface IScenario { + name: string; + type?: 'STARTER' | 'MESSAGE_TRIGGERED' | null; +} diff --git a/simulator-ui/src/main/webapp/app/scenario/scenario.routes.ts b/simulator-ui/src/main/webapp/app/scenario/scenario.routes.ts new file mode 100644 index 000000000..17291f72d --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/scenario.routes.ts @@ -0,0 +1,27 @@ +import { Routes } from '@angular/router'; + +import { ASC } from 'app/config/navigation.constants'; + +import { ScenarioComponent } from './list/scenario.component'; +import { ScenarioDetailComponent } from './detail/scenario-detail.component'; + +import ScenarioResolve from './route/scenario-parameter-routing-resolve.service'; + +const scenarioRoute: Routes = [ + { + path: '', + component: ScenarioComponent, + data: { + defaultSort: 'id,' + ASC, + }, + }, + { + path: ':name/:type/view', + component: ScenarioDetailComponent, + resolve: { + scenarioParameters: ScenarioResolve, + }, + }, +]; + +export default scenarioRoute; diff --git a/simulator-ui/src/main/webapp/app/scenario/scenario.test-samples.ts b/simulator-ui/src/main/webapp/app/scenario/scenario.test-samples.ts new file mode 100644 index 000000000..584bc2d1f --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/scenario.test-samples.ts @@ -0,0 +1,20 @@ +import { IScenario } from './scenario.model'; + +export const sampleWithRequiredData: IScenario = { + name: 'geez near', + type: 'STARTER', +}; + +export const sampleWithPartialData: IScenario = { + name: 'ah internationalise deliver', + type: 'MESSAGE_TRIGGERED', +}; + +export const sampleWithFullData: IScenario = { + name: 'oof continually after', + type: 'STARTER', +}; + +Object.freeze(sampleWithRequiredData); +Object.freeze(sampleWithPartialData); +Object.freeze(sampleWithFullData); diff --git a/simulator-ui/src/main/webapp/app/scenario/service/scenario.service.spec.ts b/simulator-ui/src/main/webapp/app/scenario/service/scenario.service.spec.ts new file mode 100644 index 000000000..94c7461fc --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/service/scenario.service.spec.ts @@ -0,0 +1,163 @@ +import { TestBed } from '@angular/core/testing'; +import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing'; + +import { sampleWithRequiredData as sampleParameterWithRequiredData } from 'app/entities/scenario-parameter/scenario-parameter.test-samples'; +import { IScenarioParameter } from 'app/entities/scenario-parameter/scenario-parameter.model'; + +import { IScenario } from '../scenario.model'; +import { ScenarioService } from './scenario.service'; +import { sampleWithFullData, sampleWithPartialData, sampleWithRequiredData } from '../scenario.test-samples'; + +describe('Scenario Service', () => { + let service: ScenarioService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + }); + + service = TestBed.inject(ScenarioService); + httpMock = TestBed.inject(HttpTestingController); + }); + + describe('Service methods', () => { + it('should find parameters', () => { + const returnedFromService: IScenarioParameter[] = [sampleParameterWithRequiredData]; + + let expectedResult: IScenarioParameter[] | null = null; + service.findParameters('scenario-name').subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'GET' }); + req.flush(returnedFromService); + + expect(expectedResult).toMatchObject(returnedFromService); + }); + + it('should launch a Scenario', () => { + const returnedFromService: number = 1234; + const scenarioName = 'scenario-name'; + const scenarioParameters: IScenarioParameter[] = []; + + let expectedResult: number | null = null; + service.launch(scenarioName, scenarioParameters).subscribe(resp => (expectedResult = resp.body)); + + const req = httpMock.expectOne({ method: 'POST' }); + req.flush(returnedFromService); + + expect(expectedResult).toEqual(returnedFromService); + }); + + describe('addScenarioToCollectionIfMissing', () => { + let expectedResult: IScenario | IScenario[] | boolean | null; + + beforeEach(() => { + expectedResult = null; + }); + + it('should add a Scenario to an empty array', () => { + const scenario: IScenario = sampleWithRequiredData; + expectedResult = service.addScenarioToCollectionIfMissing([], scenario); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(scenario); + }); + + it('should not add a Scenario to an array that contains it', () => { + const scenario: IScenario = sampleWithRequiredData; + const scenarioCollection: IScenario[] = [ + { + ...scenario, + }, + sampleWithPartialData, + ]; + expectedResult = service.addScenarioToCollectionIfMissing(scenarioCollection, scenario); + expect(expectedResult).toHaveLength(2); + }); + + it("should add a Scenario to an array that doesn't contain it", () => { + const scenario: IScenario = sampleWithRequiredData; + const scenarioCollection: IScenario[] = [sampleWithPartialData]; + expectedResult = service.addScenarioToCollectionIfMissing(scenarioCollection, scenario); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(scenario); + }); + + it('should add only unique Scenario to an array', () => { + const scenarioArray: IScenario[] = [sampleWithRequiredData, sampleWithPartialData, sampleWithFullData]; + const scenarioCollection: IScenario[] = [sampleWithRequiredData]; + expectedResult = service.addScenarioToCollectionIfMissing(scenarioCollection, ...scenarioArray); + expect(expectedResult).toHaveLength(3); + }); + + it('should accept varargs', () => { + const scenario: IScenario = sampleWithRequiredData; + const scenario2: IScenario = sampleWithPartialData; + expectedResult = service.addScenarioToCollectionIfMissing([], scenario, scenario2); + expect(expectedResult).toHaveLength(2); + expect(expectedResult).toContain(scenario); + expect(expectedResult).toContain(scenario2); + }); + + it('should accept null and undefined values', () => { + const scenario: IScenario = sampleWithRequiredData; + expectedResult = service.addScenarioToCollectionIfMissing([], null, scenario, undefined); + expect(expectedResult).toHaveLength(1); + expect(expectedResult).toContain(scenario); + }); + + it('should return initial array if no Scenario is added', () => { + const scenarioCollection: IScenario[] = [sampleWithRequiredData]; + expectedResult = service.addScenarioToCollectionIfMissing(scenarioCollection, undefined, null); + expect(expectedResult).toEqual(scenarioCollection); + }); + }); + + describe('compareScenario', () => { + it('Should return true if both entities are null', () => { + const entity1 = null; + const entity2 = null; + + const compareResult = service.compareScenario(entity1, entity2); + + expect(compareResult).toEqual(true); + }); + + it('Should return false if one entity is null', () => { + const entity1 = { name: 'name' }; + const entity2 = null; + + const compareResult1 = service.compareScenario(entity1, entity2); + const compareResult2 = service.compareScenario(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey differs', () => { + const entity1 = { name: 'name' }; + const entity2 = { name: 'another-name' }; + + const compareResult1 = service.compareScenario(entity1, entity2); + const compareResult2 = service.compareScenario(entity2, entity1); + + expect(compareResult1).toEqual(false); + expect(compareResult2).toEqual(false); + }); + + it('Should return false if primaryKey matches', () => { + const entity1 = { name: 'name' }; + const entity2 = { name: 'name' }; + + const compareResult1 = service.compareScenario(entity1, entity2); + const compareResult2 = service.compareScenario(entity2, entity1); + + expect(compareResult1).toEqual(true); + expect(compareResult2).toEqual(true); + }); + }); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/simulator-ui/src/main/webapp/app/scenario/service/scenario.service.ts b/simulator-ui/src/main/webapp/app/scenario/service/scenario.service.ts new file mode 100644 index 000000000..3dfc8c646 --- /dev/null +++ b/simulator-ui/src/main/webapp/app/scenario/service/scenario.service.ts @@ -0,0 +1,67 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpResponse } from '@angular/common/http'; + +import { Observable } from 'rxjs'; + +import { ApplicationConfigService } from 'app/core/config/application-config.service'; +import { createRequestOption } from 'app/core/request/request-util'; +import { isPresent } from 'app/core/util/operators'; + +import { IScenario } from '../scenario.model'; +import { IScenarioParameter } from '../../entities/scenario-parameter/scenario-parameter.model'; + +export type RestScenario = IScenario; + +export type EntityResponseType = HttpResponse; +export type EntityArrayResponseType = HttpResponse; + +@Injectable({ providedIn: 'root' }) +export class ScenarioService { + protected resourceUrl = this.applicationConfigService.getEndpointFor('api/scenarios'); + + constructor( + protected http: HttpClient, + protected applicationConfigService: ApplicationConfigService, + ) {} + + findParameters(name: string): Observable> { + return this.http.get(`${this.resourceUrl}/${name}/parameters`, { observe: 'response' }); + } + + launch(name: string, parameters: IScenarioParameter[] = []): Observable> { + return this.http.post(`${this.resourceUrl}/${name}/launch`, parameters, { observe: 'response' }); + } + + query(req?: any): Observable { + const options = createRequestOption(req); + return this.http.get(this.resourceUrl, { params: options, observe: 'response' }); + } + + getScenarioIdentifier(scenario: Pick): string { + return scenario.name; + } + + compareScenario(o1: Pick | null, o2: Pick | null): boolean { + return o1 && o2 ? this.getScenarioIdentifier(o1) === this.getScenarioIdentifier(o2) : o1 === o2; + } + + addScenarioToCollectionIfMissing>( + scenarioCollection: Type[], + ...scenariosToCheck: (Type | null | undefined)[] + ): Type[] { + const scenarios: Type[] = scenariosToCheck.filter(isPresent); + if (scenarios.length > 0) { + const scenarioCollectionIdentifiers = scenarioCollection.map(scenarioItem => this.getScenarioIdentifier(scenarioItem)!); + const scenariosToAdd = scenarios.filter(scenarioItem => { + const scenarioIdentifier = this.getScenarioIdentifier(scenarioItem); + if (scenarioCollectionIdentifiers.includes(scenarioIdentifier)) { + return false; + } + scenarioCollectionIdentifiers.push(scenarioIdentifier); + return true; + }); + return [...scenariosToAdd, ...scenarioCollection]; + } + return scenarioCollection; + } +} diff --git a/simulator-ui/src/main/webapp/i18n/en/scenario.json b/simulator-ui/src/main/webapp/i18n/en/scenario.json new file mode 100644 index 000000000..abd232005 --- /dev/null +++ b/simulator-ui/src/main/webapp/i18n/en/scenario.json @@ -0,0 +1,23 @@ +{ + "citrusSimulatorApp": { + "scenario": { + "home": { + "title": "Scenarios", + "refreshListLabel": "Refresh list", + "notFound": "No Scenarios found" + }, + "detail": { + "title": "Scenario" + }, + "id": "ID", + "name": "Name", + "type": "Type", + "scenarioParameters": "Scenario Parameters", + "action": { + "launch": "Launch", + "launchFailed": "Failed to launch Scenario!", + "launchedSuccessfully": "Scenario successfully launched: View Execution." + } + } + } +} diff --git a/simulator-ui/src/main/webapp/i18n/en/testResult.json b/simulator-ui/src/main/webapp/i18n/en/testResult.json index e41034045..2086563e3 100644 --- a/simulator-ui/src/main/webapp/i18n/en/testResult.json +++ b/simulator-ui/src/main/webapp/i18n/en/testResult.json @@ -16,7 +16,7 @@ "errorMessage": "Error Message", "failureStack": "Failure Stack", "failureType": "Failure Type", - "testParameter": "Test Parameter", + "testParameter": "Test Parameters", "createdDate": "Created Date", "lastModifiedDate": "Last Modified Date" }