content: /*
   * Copyright 2017-2025 Hyland Software, Inc. and its affiliates.
   *
   * 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.activiti.cloud.starter.tests;
  
  import static org.activiti.cloud.services.query.model.IntegrationContextEntity.ERROR_MESSAGE_LENGTH;
  import static org.assertj.core.api.Assertions.assertThat;
  import static org.assertj.core.api.Assertions.tuple;
  import static org.awaitility.Awaitility.await;
  
  import java.io.IOException;
  import java.nio.charset.StandardCharsets;
  import java.nio.file.Files;
  import java.nio.file.Paths;
  import java.util.Arrays;
  import java.util.UUID;
  import org.activiti.api.process.model.BPMNActivity;
  import org.activiti.api.process.model.IntegrationContext;
  import org.activiti.api.process.model.ProcessInstance;
  import org.activiti.api.runtime.model.impl.BPMNActivityImpl;
  import org.activiti.api.runtime.model.impl.BPMNSequenceFlowImpl;
  import org.activiti.api.runtime.model.impl.IntegrationContextImpl;
  import org.activiti.api.runtime.model.impl.ProcessDefinitionImpl;
  import org.activiti.api.runtime.model.impl.ProcessInstanceImpl;
  import org.activiti.cloud.api.process.model.CloudBPMNActivity;
  import org.activiti.cloud.api.process.model.CloudBPMNActivity.BPMNActivityStatus;
  import org.activiti.cloud.api.process.model.CloudBpmnError;
  import org.activiti.cloud.api.process.model.CloudIntegrationContext;
  import org.activiti.cloud.api.process.model.CloudIntegrationContext.IntegrationContextStatus;
  import org.activiti.cloud.api.process.model.CloudServiceTask;
  import org.activiti.cloud.api.process.model.impl.events.CloudBPMNActivityCompletedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudBPMNActivityStartedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudIntegrationErrorReceivedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudIntegrationRequestedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudIntegrationResultReceivedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudProcessCreatedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudProcessDeployedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudProcessStartedEventImpl;
  import org.activiti.cloud.api.process.model.impl.events.CloudSequenceFlowTakenEventImpl;
  import org.activiti.cloud.services.query.app.repository.BPMNActivityRepository;
  import org.activiti.cloud.services.query.app.repository.BPMNSequenceFlowRepository;
  import org.activiti.cloud.services.query.app.repository.IntegrationContextRepository;
  import org.activiti.cloud.services.query.app.repository.ProcessDefinitionRepository;
  import org.activiti.cloud.services.query.app.repository.ProcessInstanceRepository;
  import org.activiti.cloud.services.query.app.repository.ProcessModelRepository;
  import org.activiti.cloud.services.query.model.StringUtils;
  import org.activiti.cloud.services.test.containers.KeycloakContainerApplicationInitializer;
  import org.activiti.cloud.services.test.identity.IdentityTokenProducer;
  import org.activiti.cloud.starters.test.EventsAggregator;
  import org.activiti.cloud.starters.test.MyProducer;
  import org.activiti.cloud.starters.test.builder.ProcessInstanceEventContainedBuilder;
  import org.junit.jupiter.api.AfterEach;
  import org.junit.jupiter.api.BeforeEach;
  import org.junit.jupiter.api.Test;
  import org.springframework.beans.factory.annotation.Autowired;
  import org.springframework.boot.test.context.SpringBootTest;
  import org.springframework.boot.test.web.client.TestRestTemplate;
  import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
  import org.springframework.context.annotation.Import;
  import org.springframework.core.ParameterizedTypeReference;
  import org.springframework.hateoas.PagedModel;
  import org.springframework.http.HttpMethod;
  import org.springframework.http.HttpStatus;
  import org.springframework.http.ResponseEntity;
  import org.springframework.test.annotation.DirtiesContext;
  import org.springframework.test.context.ContextConfiguration;
  import org.springframework.test.context.TestPropertySource;
  
  @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
  @TestPropertySource("classpath:application-test-admin.properties")
  @ContextConfiguration(initializers = { KeycloakContainerApplicationInitializer.class })
  @Import(TestChannelBinderConfiguration.class)
  @DirtiesContext
  public class QueryAdminProcessServiceTasksIT {
  
      private static final String ERROR_MESSAGE =
          "An error occurred consuming ACS API with inputs {targetFolder={}, action=CREATE_FILE}. Cause: [405] during [GET] to [https://aae-3734-env.envalfresco.com/alfresco/api/-default-/public/alfresco/versions/1/nodes/] [NodesApiClient#getNode(String,List,String,List)]: [{\"error\":{\"errorKey\":\"framework.exception.UnsupportedResourceOperation\",\"statusCode\":405,\"briefSummary\":\"09070282 The operation is unsupported\",\"stackTrace\":\"For security reasons the stack trace is no longer displayed, but the property is kept for previous versions\",\"descriptionURL\":\"https://api-explorer.alfresco.com\"}}]";
  
      private static final String SERVICE_TASK_ELEMENT_ID = "sid-CDFE7219-4627-43E9-8CA8-866CC38EBA94";
  
      private static final String SERVICE_TASK_TYPE = "serviceTask";
  
      private static final String PROC_URL = "/admin/v1/process-instances";
  
      private static final ParameterizedTypeReference<PagedModel<CloudServiceTask>> PAGED_TASKS_RESPONSE_TYPE = new ParameterizedTypeReference<PagedModel<CloudServiceTask>>() {};
  
      private static final ParameterizedTypeReference<CloudServiceTask> SINGLE_TASK_RESPONSE_TYPE = new ParameterizedTypeReference<CloudServiceTask>() {};
  
      private static final ParameterizedTypeReference<CloudIntegrationContext> SINGLE_INT_CONTEXT_RESPONSE_TYPE = new ParameterizedTypeReference<CloudIntegrationContext>() {};
      public static final String SERVICE_TASKS_URL = "/admin/v1/service-tasks";
  
      @Autowired
      private IdentityTokenProducer identityTokenProducer;
  
      @Autowired
      private ProcessDefinitionRepository processDefinitionRepository;
  
      @Autowired
      private ProcessModelRepository processModelRepository;
  
      @Autowired
      private ProcessInstanceRepository processInstanceRepository;
  
      @Autowired
      private BPMNActivityRepository bpmnActivityRepository;
  
      @Autowired
      private BPMNSequenceFlowRepository bpmnSequenceFlowRepository;
  
      @Autowired
      private IntegrationContextRepository integrationContextRepository;
  
      @Autowired
      private MyProducer producer;
  
      @Autowired
      private TestRestTemplate testRestTemplate;
  
      private String processDefinitionId = UUID.randomUUID().toString();
  
      private ProcessInstanceEventContainedBuilder processInstanceBuilder;
  
      private EventsAggregator eventsAggregator;
  
      @BeforeEach
      public void setUp() throws IOException {
          identityTokenProducer.withTestUser("hradmin");
  
          eventsAggregator = new EventsAggregator(producer);
  
          processInstanceBuilder = new ProcessInstanceEventContainedBuilder(eventsAggregator);
  
          //given
          ProcessDefinitionImpl firstProcessDefinition = new ProcessDefinitionImpl();
          firstProcessDefinition.setId(processDefinitionId);
          firstProcessDefinition.setKey("mySimpleProcess");
          firstProcessDefinition.setName("My Simple Process");
  
          CloudProcessDeployedEventImpl firstProcessDeployedEvent = new CloudProcessDeployedEventImpl(
              firstProcessDefinition
          );
          firstProcessDeployedEvent.setProcessModelContent(
              new String(
                  Files.readAllBytes(Paths.get("src/test/resources/parse-for-test/SimpleProcess.bpmn20.xml")),
                  StandardCharsets.UTF_8
              )
          );
  
          producer.send(firstProcessDeployedEvent);
      }
  
      @AfterEach
      public void tearDown() {
          processModelRepository.deleteAll();
          processDefinitionRepository.deleteAll();
          processInstanceRepository.deleteAll();
          integrationContextRepository.deleteAll();
          bpmnActivityRepository.deleteAll();
          bpmnSequenceFlowRepository.deleteAll();
      }
  
      @Test
      public void shouldGetProcessInstanceServiceTasks() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      PROC_URL + "/" + process.getId() + "/service-tasks",
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody().getContent())
                      .hasSize(1)
                      .extracting(CloudServiceTask::getActivityType)
                      .contains(SERVICE_TASK_TYPE);
              });
      }
  
      @Test
      public void shouldGetProcessInstanceServiceTasksByStatus() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      PROC_URL + "/" + process.getId() + "/service-tasks?status={status}",
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE,
                      CloudBPMNActivity.BPMNActivityStatus.STARTED
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody().getContent())
                      .hasSize(1)
                      .extracting(CloudServiceTask::getStatus, CloudServiceTask::getActivityType)
                      .contains(tuple(CloudBPMNActivity.BPMNActivityStatus.STARTED, SERVICE_TASK_TYPE));
              });
  
          // and given
          BPMNActivityImpl taskActivity = new BPMNActivityImpl(
              SERVICE_TASK_ELEMENT_ID,
              "Service Task",
              SERVICE_TASK_TYPE
          );
          taskActivity.setProcessDefinitionId(process.getProcessDefinitionId());
          taskActivity.setProcessInstanceId(process.getId());
          taskActivity.setExecutionId(UUID.randomUUID().toString());
  
          sendActivityCompletedEvent(taskActivity, process);
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      PROC_URL + "/" + process.getId() + "/service-tasks?status={status}",
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE,
                      CloudBPMNActivity.BPMNActivityStatus.COMPLETED
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody().getContent())
                      .hasSize(1)
                      .extracting(CloudServiceTask::getStatus, CloudServiceTask::getActivityType)
                      .contains(tuple(CloudBPMNActivity.BPMNActivityStatus.COMPLETED, SERVICE_TASK_TYPE));
              });
      }
  
      @Test
      public void shouldGetServiceTasks() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      SERVICE_TASKS_URL,
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody().getContent())
                      .hasSize(1)
                      .extracting(CloudServiceTask::getActivityType)
                      .contains(SERVICE_TASK_TYPE);
              });
      }
  
      @Test
      void shouldReturn400WhenGetServiceTasksByStatusWithInvalidStatus() {
          ResponseEntity<PagedModel<CloudServiceTask>> running = testRestTemplate.exchange(
              "/admin/v1/service-tasks?status={status}",
              HttpMethod.GET,
              identityTokenProducer.entityWithAuthorizationHeader(),
              PAGED_TASKS_RESPONSE_TYPE,
              "RUNNING"
          );
          assertThat(running.getStatusCode()).isEqualTo(HttpStatus.BAD_REQUEST);
      }
  
      @Test
      public void shouldGetServiceTasksByStatus() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      "/admin/v1/service-tasks?status={status}",
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE,
                      CloudBPMNActivity.BPMNActivityStatus.STARTED
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody().getContent())
                      .hasSize(1)
                      .extracting(CloudServiceTask::getStatus, CloudServiceTask::getActivityType)
                      .contains(tuple(CloudBPMNActivity.BPMNActivityStatus.STARTED, SERVICE_TASK_TYPE));
              });
  
          // and given
          BPMNActivityImpl taskActivity = new BPMNActivityImpl(
              SERVICE_TASK_ELEMENT_ID,
              "Service Task",
              SERVICE_TASK_TYPE
          );
          taskActivity.setProcessDefinitionId(process.getProcessDefinitionId());
          taskActivity.setProcessInstanceId(process.getId());
          taskActivity.setExecutionId(UUID.randomUUID().toString());
  
          sendActivityCompletedEvent(taskActivity, process);
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      "/admin/v1/service-tasks?status={status}",
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE,
                      CloudBPMNActivity.BPMNActivityStatus.COMPLETED
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody().getContent())
                      .hasSize(1)
                      .extracting(CloudServiceTask::getStatus, CloudServiceTask::getActivityType)
                      .contains(tuple(CloudBPMNActivity.BPMNActivityStatus.COMPLETED, SERVICE_TASK_TYPE));
              });
      }
  
      @Test
      public void shouldGetServiceTaskById() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          await()
              .untilAsserted(() -> {
                  ResponseEntity<PagedModel<CloudServiceTask>> serviceTasksResponse = testRestTemplate.exchange(
                      SERVICE_TASKS_URL,
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE
                  );
  
                  assertThat(serviceTasksResponse.getBody().getContent()).isNotEmpty();
  
                  String serviceTaskId = serviceTasksResponse.getBody().getContent().iterator().next().getId();
  
                  //when
                  ResponseEntity<CloudServiceTask> responseEntity = testRestTemplate.exchange(
                      "/admin/v1/service-tasks/" + serviceTaskId,
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      SINGLE_TASK_RESPONSE_TYPE
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
                  assertThat(responseEntity.getBody()).isNotNull();
                  assertThat(responseEntity.getBody())
                      .extracting(
                          CloudServiceTask::getId,
                          CloudServiceTask::getElementId,
                          CloudServiceTask::getActivityType
                      )
                      .containsExactly(serviceTaskId, SERVICE_TASK_ELEMENT_ID, SERVICE_TASK_TYPE);
              });
      }
  
      @Test
      public void shouldGetServiceTaskIntegrationContextErrorById() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          ResponseEntity<PagedModel<CloudServiceTask>> serviceTasksResponse = testRestTemplate.exchange(
              SERVICE_TASKS_URL,
              HttpMethod.GET,
              identityTokenProducer.entityWithAuthorizationHeader(),
              PAGED_TASKS_RESPONSE_TYPE
          );
  
          assertThat(serviceTasksResponse.getBody().getContent()).isNotEmpty();
  
          CloudBPMNActivity serviceTask = serviceTasksResponse.getBody().getContent().iterator().next();
  
          final String rootProcessInstanceId = UUID.randomUUID().toString();
          final IntegrationContext integrationContext = buildIntegrationContext(
              process,
              rootProcessInstanceId,
              serviceTask
          );
  
          sendIntegrationRequestedEvent(integrationContext);
  
          //when
          await()
              .untilAsserted(() -> {
                  CloudIntegrationContext cloudIntegrationContext = retrieveIntegrationContext(serviceTask.getId());
                  assertThat(cloudIntegrationContext)
                      .extracting(
                          CloudIntegrationContext::getClientId,
                          CloudIntegrationContext::getClientType,
                          CloudIntegrationContext::getRootProcessInstanceId,
                          CloudIntegrationContext::getStatus
                      )
                      .containsExactly(
                          SERVICE_TASK_ELEMENT_ID,
                          SERVICE_TASK_TYPE,
                          rootProcessInstanceId,
                          IntegrationContextStatus.INTEGRATION_REQUESTED
                      );
              });
  
          // and given
          Throwable cause = new RuntimeException(ERROR_MESSAGE);
          CloudBpmnError error = new CloudBpmnError("ERROR_CODE", cause);
  
          eventsAggregator.addEvents(
              new CloudIntegrationErrorReceivedEventImpl(
                  integrationContext,
                  error.getErrorCode(),
                  error.getMessage(),
                  error.getClass().getName(),
                  Arrays.asList(error.getCause().getStackTrace())
              )
          );
          eventsAggregator.sendAll();
  
          await()
              .untilAsserted(() -> {
                  CloudIntegrationContext cloudIntegrationContext = retrieveIntegrationContext(serviceTask.getId());
                  assertThat(cloudIntegrationContext)
                      .extracting(
                          CloudIntegrationContext::getClientId,
                          CloudIntegrationContext::getClientType,
                          CloudIntegrationContext::getStatus,
                          CloudIntegrationContext::getErrorCode,
                          CloudIntegrationContext::getErrorMessage,
                          CloudIntegrationContext::getErrorClassName
                      )
                      .containsExactly(
                          SERVICE_TASK_ELEMENT_ID,
                          SERVICE_TASK_TYPE,
                          IntegrationContextStatus.INTEGRATION_ERROR_RECEIVED,
                          error.getErrorCode(),
                          StringUtils.truncate(error.getMessage(), ERROR_MESSAGE_LENGTH),
                          error.getClass().getName()
                      );
  
                  assertThat(cloudIntegrationContext.getStackTraceElements()).isNotEmpty();
              });
      }
  
      private IntegrationContextImpl buildIntegrationContext(
          ProcessInstance process,
          String rootProcessInstanceId,
          CloudBPMNActivity serviceTask
      ) {
          IntegrationContextImpl integrationContext = new IntegrationContextImpl();
          integrationContext.setProcessInstanceId(process.getId());
          integrationContext.setRootProcessInstanceId(rootProcessInstanceId);
          integrationContext.setExecutionId(serviceTask.getExecutionId());
          integrationContext.setClientId(serviceTask.getElementId());
          integrationContext.setClientType(serviceTask.getActivityType());
          integrationContext.setClientName(serviceTask.getActivityName());
          integrationContext.setProcessDefinitionId(process.getProcessDefinitionId());
          integrationContext.addInBoundVariable("key", "value");
          return integrationContext;
      }
  
      @Test
      public void shouldGetServiceTaskIntegrationContextResultById() throws InterruptedException {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          CloudServiceTask serviceTask = waitForServiceTask();
          final String rootProcessInstanceId = UUID.randomUUID().toString();
          IntegrationContext integrationContext = buildIntegrationContext(process, rootProcessInstanceId, serviceTask);
  
          sendIntegrationRequestedEvent(integrationContext);
  
          //when
          await()
              .untilAsserted(() -> {
                  CloudIntegrationContext cloudIntegrationContext = retrieveIntegrationContext(serviceTask.getId());
                  assertThat(cloudIntegrationContext)
                      .extracting(
                          CloudIntegrationContext::getClientId,
                          CloudIntegrationContext::getClientType,
                          CloudIntegrationContext::getRootProcessInstanceId,
                          CloudIntegrationContext::getStatus
                      )
                      .containsExactly(
                          SERVICE_TASK_ELEMENT_ID,
                          SERVICE_TASK_TYPE,
                          rootProcessInstanceId,
                          IntegrationContextStatus.INTEGRATION_REQUESTED
                      );
              });
  
          // and given
          sendIntegrationResultReceivedEvent(integrationContext);
  
          await()
              .untilAsserted(() -> {
                  CloudIntegrationContext cloudIntegrationContext = retrieveIntegrationContext(serviceTask.getId());
                  assertThat(cloudIntegrationContext)
                      .extracting(
                          CloudIntegrationContext::getClientId,
                          CloudIntegrationContext::getClientType,
                          CloudIntegrationContext::getStatus
                      )
                      .containsExactly(
                          SERVICE_TASK_ELEMENT_ID,
                          SERVICE_TASK_TYPE,
                          IntegrationContextStatus.INTEGRATION_RESULT_RECEIVED
                      );
              });
      }
  
      private CloudIntegrationContext retrieveIntegrationContext(String serviceTaskId) {
          ResponseEntity<CloudIntegrationContext> responseEntity = testRestTemplate.exchange(
              "/admin/v1/service-tasks/{serviceTaskId}/integration-context",
              HttpMethod.GET,
              identityTokenProducer.entityWithAuthorizationHeader(),
              SINGLE_INT_CONTEXT_RESPONSE_TYPE,
              serviceTaskId
          );
          assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
          assertThat(responseEntity.getBody()).isNotNull();
          return responseEntity.getBody();
      }
  
      private void sendIntegrationResultReceivedEvent(IntegrationContext integrationContext) {
          eventsAggregator.addEvents(new CloudIntegrationResultReceivedEventImpl(integrationContext));
  
          eventsAggregator.sendAll();
      }
  
      private void sendIntegrationRequestedEvent(IntegrationContext integrationContext) {
          eventsAggregator.addEvents(new CloudIntegrationRequestedEventImpl(integrationContext));
  
          eventsAggregator.sendAll();
      }
  
      private CloudServiceTask waitForServiceTask(BPMNActivityStatus status) {
          await()
              .untilAsserted(() -> {
                  final PagedModel<CloudServiceTask> page = testRestTemplate
                      .exchange(
                          SERVICE_TASKS_URL,
                          HttpMethod.GET,
                          identityTokenProducer.entityWithAuthorizationHeader(),
                          PAGED_TASKS_RESPONSE_TYPE
                      )
                      .getBody();
                  assertThat(page).isNotEmpty();
                  final CloudServiceTask serviceTask = page.getContent().iterator().next();
                  assertThat(serviceTask.getStatus()).isEqualTo(status);
              });
          return retrieveServiceTask();
      }
  
      private CloudServiceTask waitForServiceTask() {
          return waitForServiceTask(BPMNActivityStatus.STARTED);
      }
  
      private CloudServiceTask retrieveServiceTask() {
          ResponseEntity<PagedModel<CloudServiceTask>> serviceTasksResponse = testRestTemplate.exchange(
              SERVICE_TASKS_URL,
              HttpMethod.GET,
              identityTokenProducer.entityWithAuthorizationHeader(),
              PAGED_TASKS_RESPONSE_TYPE
          );
  
          assertThat(serviceTasksResponse.getBody()).isNotEmpty();
  
          return serviceTasksResponse.getBody().getContent().iterator().next();
      }
  
      @Test
      public void should_supportLoopInvolvingServiceTasks() {
          //given
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
          CloudServiceTask serviceTaskIt1 = waitForServiceTask();
  
          final String rootProcessInstanceId = UUID.randomUUID().toString();
          IntegrationContextImpl integrationContext = buildIntegrationContext(
              process,
              rootProcessInstanceId,
              serviceTaskIt1
          );
  
          sendIntegrationRequestedEvent(integrationContext);
          sendIntegrationResultReceivedEvent(integrationContext);
          sendActivityCompletedEvent(serviceTaskIt1, process);
  
          waitForServiceTask(BPMNActivityStatus.COMPLETED);
          waitForIntegrationContext(serviceTaskIt1, IntegrationContextStatus.INTEGRATION_RESULT_RECEIVED);
  
          //when the process loop back and reaches the task a second time
          final BPMNActivityImpl bpmnActivity = buildServiceTask(serviceTaskIt1.getExecutionId(), process);
          sendActivitiStartedEvent(process, bpmnActivity);
  
          final CloudServiceTask serviceTask = waitForServiceTask(BPMNActivityStatus.STARTED);
          IntegrationContextImpl integrationContextIt2 = buildIntegrationContext(
              process,
              rootProcessInstanceId,
              serviceTask
          );
          sendIntegrationRequestedEvent(integrationContextIt2);
  
          //then
          waitForIntegrationContext(serviceTaskIt1, IntegrationContextStatus.INTEGRATION_REQUESTED);
      }
  
      private void sendActivitiStartedEvent(ProcessInstanceImpl process, BPMNActivityImpl bpmnActivity) {
          eventsAggregator.addEvents(
              new CloudBPMNActivityStartedEventImpl(bpmnActivity, process.getProcessDefinitionId(), process.getId())
          );
  
          eventsAggregator.sendAll();
      }
  
      private void sendActivityCompletedEvent(BPMNActivity bpmnActivity, ProcessInstance processInstance) {
          final CloudBPMNActivityCompletedEventImpl activityCompletedEvent = new CloudBPMNActivityCompletedEventImpl(
              bpmnActivity,
              processInstance.getProcessDefinitionId(),
              processInstance.getId()
          );
          eventsAggregator.addEvents(activityCompletedEvent);
          eventsAggregator.sendAll();
      }
  
      private void waitForIntegrationContext(CloudServiceTask serviceTask, IntegrationContextStatus status) {
          await()
              .untilAsserted(() -> {
                  CloudIntegrationContext cloudIntegrationContext = retrieveIntegrationContext(serviceTask.getId());
                  assertThat(cloudIntegrationContext.getStatus()).isEqualTo(status);
              });
      }
  
      private ProcessInstanceImpl sendEventsForStartSimpleProcessInstance() {
          ProcessInstanceImpl process = startSimpleProcessInstance();
          eventsAggregator.sendAll();
          return process;
      }
  
      @Test
      public void shouldNotGetProcessInstanceServiceTasks() throws InterruptedException {
          //given
          identityTokenProducer.withTestUser("hruser");
  
          ProcessInstanceImpl process = sendEventsForStartSimpleProcessInstance();
  
          //then
          await()
              .untilAsserted(() -> {
                  assertThat(bpmnActivityRepository.findByProcessInstanceId(process.getId())).hasSize(2);
                  assertThat(bpmnSequenceFlowRepository.findByProcessInstanceId(process.getId())).hasSize(1);
              });
  
          await()
              .untilAsserted(() -> {
                  //when
                  ResponseEntity<PagedModel<CloudServiceTask>> responseEntity = testRestTemplate.exchange(
                      PROC_URL + "/" + process.getId() + "/service-tasks",
                      HttpMethod.GET,
                      identityTokenProducer.entityWithAuthorizationHeader(),
                      PAGED_TASKS_RESPONSE_TYPE
                  );
                  //then
                  assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN);
              });
      }
  
      private ProcessInstanceImpl startSimpleProcessInstance() {
          String executionId = UUID.randomUUID().toString();
          ProcessInstanceImpl process = new ProcessInstanceImpl();
          process.setId(UUID.randomUUID().toString());
          process.setName("process");
          process.setProcessDefinitionKey("mySimpleProcess");
          process.setProcessDefinitionId(processDefinitionId);
          process.setProcessDefinitionVersion(1);
  
          BPMNActivityImpl startActivity = new BPMNActivityImpl("startEvent1", "", "startEvent");
          startActivity.setProcessDefinitionId(process.getProcessDefinitionId());
          startActivity.setProcessInstanceId(process.getId());
          startActivity.setExecutionId(executionId);
  
          BPMNSequenceFlowImpl sequenceFlow = new BPMNSequenceFlowImpl(
              "sid-68945AF1-396F-4B8A-B836-FC318F62313F",
              "startEvent1",
              SERVICE_TASK_ELEMENT_ID
          );
          sequenceFlow.setProcessDefinitionId(process.getProcessDefinitionId());
          sequenceFlow.setProcessInstanceId(process.getId());
  
          BPMNActivityImpl activity = buildServiceTask(executionId, process);
  
          eventsAggregator.addEvents(
              new CloudProcessCreatedEventImpl(process),
              new CloudProcessStartedEventImpl(process, null, null),
              new CloudBPMNActivityStartedEventImpl(startActivity, processDefinitionId, process.getId()),
              new CloudBPMNActivityCompletedEventImpl(startActivity, processDefinitionId, process.getId()),
              new CloudSequenceFlowTakenEventImpl(sequenceFlow),
              new CloudBPMNActivityStartedEventImpl(activity, processDefinitionId, process.getId())
          );
  
          return process;
      }
  
      private BPMNActivityImpl buildServiceTask(String executionId, ProcessInstanceImpl process) {
          BPMNActivityImpl activity = new BPMNActivityImpl(SERVICE_TASK_ELEMENT_ID, "Service Task", SERVICE_TASK_TYPE);
          activity.setProcessDefinitionId(process.getProcessDefinitionId());
          activity.setProcessInstanceId(process.getId());
          activity.setExecutionId(executionId);
          return activity;
      }
  }
  