diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 5469cdeb5..2dd62c911 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -33,7 +33,7 @@ jobs: - name: Build id: build - run: mvn -B -U "-Dsurefire.rerunFailingTestsCount=5" clean install + run: mvn -B -U -P regular "-Dsurefire.rerunFailingTestsCount=5" clean install - name: Archive Test Results uses: actions/upload-artifact@v2 @@ -43,7 +43,63 @@ jobs: path: "*/target/surefire-reports/" retention-days: 7 -# We need to upload the event file as an artifact in order to support publishing the results of + build-and-test-testcontainers: + name: Run tests on ubuntu-latest using testcontainers + timeout-minutes: 15 + runs-on: ubuntu-latest + env: + IMAGE_NAME: "qa-tests" + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Set up Java environment + uses: actions/setup-java@v2.5.0 + with: + distribution: temurin + java-version: 11 # Change to java 8 when tests are properly separated (https://github.com/camunda-cloud/zeebe-process-test/issues/195) + + - name: Package + run: mvn -B -U clean package -DskipTests + + - name: Build engine docker container + run: | + cd engine-agent + docker build . -t ${{ env.IMAGE_NAME }}:${{ github.sha }} + + - name: Update image tag in config + run: | + cd extension-testcontainer/src/main/resources + sed -i '/container.image.name=/ s/=.*/=${{ env.IMAGE_NAME }}/' config.properties + sed -i '/container.image.tag=/ s/=.*/=${{ github.sha }}/' config.properties + cat config.properties + env: + IMAGE_NAME_KEY: container.image.name + IMAGE_TAG_KEY: container.image.tag + + - name: Build + id: build + run: mvn -B -U -P testcontainer "-Dsurefire.rerunFailingTestsCount=5" install + + - name: Archive Test Results + uses: actions/upload-artifact@v2 + if: always() + with: + name: Unit Test Results Testcontainers + path: "*/target/surefire-reports/" + retention-days: 7 + + + # We need to upload the event file as an artifact in order to support publishing the results of # forked repositories (https://github.com/EnricoMi/publish-unit-test-result-action#support-fork-repositories-and-dependabot-branches) event_file: name: "Event File" @@ -55,3 +111,4 @@ jobs: name: Event File path: ${{ github.event_path }} retention-days: 1 + diff --git a/.github/workflows/deploy-artifact.yml b/.github/workflows/deploy-artifact.yml index febb7c3ef..24e1e7afa 100644 --- a/.github/workflows/deploy-artifact.yml +++ b/.github/workflows/deploy-artifact.yml @@ -15,9 +15,85 @@ jobs: test: uses: ./.github/workflows/build-test.yml - deploy-to-maven: + deploy-to-docker: runs-on: ubuntu-latest needs: [test] + + env: + OWNER: "camunda" + IMAGE_NAME: "zeebe-process-test-engine" + TAG: "latest" + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Cache + uses: actions/cache@v2 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + + - name: Import Secrets + id: secrets + uses: hashicorp/vault-action@v2.4.0 + with: + url: ${{ secrets.VAULT_ADDR }} + method: approle + roleId: ${{ secrets.VAULT_ROLE_ID }} + secretId: ${{ secrets.VAULT_SECRET_ID }} + secrets: | + secret/data/common/github.com/actions/camunda-cloud/zeebe-process-test REGISTRY_HUB_DOCKER_COM_USR; + secret/data/common/github.com/actions/camunda-cloud/zeebe-process-test REGISTRY_HUB_DOCKER_COM_PSW; + + - name: Set up Java environment + uses: actions/setup-java@v2.5.0 + with: + distribution: temurin + java-version: 11 + + - name: Build jar + run: | + cd engine-agent + mvn clean package -DskipTests + + - name: Build Docker image + run: | + if ! [ -z "${{ github.event.release.tag_name }}" ] && ! grep -q "$TAG" <<< "SNAPSHOT"; then + TAG="${{ github.event.release.tag_name }}" + fi + cd engine-agent + docker build . -t $IMAGE_NAME:$TAG + + - name: Push Docker image + run: | + echo '${{ steps.secrets.outputs.REGISTRY_HUB_DOCKER_COM_PSW }}' | docker login -u '${{ steps.secrets.outputs.REGISTRY_HUB_DOCKER_COM_USR }}' --password-stdin + echo $IMAGE_NAME + echo $TAG + IMAGE_ID=$OWNER/$IMAGE_NAME + echo $IMAGE_ID + docker tag $IMAGE_NAME $IMAGE_ID:$TAG + docker push $IMAGE_ID:$TAG + + - name: Update image and tag in Git + run : | + git config --global user.name "Release Bot" + git config --global user.email actions@github.com + git pull + cd extension-testcontainer/src/main/resources + IMAGE_ID=$OWNER/$IMAGE_NAME + sed -i '/container.image.name=/ s/=.*/=$IMAGE_ID/' config.properties + sed -i '/container.image.tag=/ s/=.*/=$TAG/' config.properties + if [[ `git status --porcelain --untracked-files=no` ]]; then + git commit -am "(release): update image and tag ($IMAGE_ID:$TAG)" + git push + fi + + deploy-to-maven: + runs-on: ubuntu-latest + needs: [deploy-to-docker] steps: - name: Checkout uses: actions/checkout@v2 @@ -83,65 +159,3 @@ jobs: asset_path: ${{ steps.release.outputs.artifacts_archive_path }} asset_name: ${{ steps.release.outputs.artifacts_archive_path }} asset_content_type: application/zip - - deploy-to-docker: - runs-on: ubuntu-latest - needs: [test] - - env: - OWNER: "camunda" - IMAGE_NAME: "zeebe-process-test-engine" - TAG: "latest" - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Cache - uses: actions/cache@v2 - with: - path: ~/.m2/repository - key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} - restore-keys: | - ${{ runner.os }}-maven- - - - name: Import Secrets - id: secrets - uses: hashicorp/vault-action@v2.4.0 - with: - url: ${{ secrets.VAULT_ADDR }} - method: approle - roleId: ${{ secrets.VAULT_ROLE_ID }} - secretId: ${{ secrets.VAULT_SECRET_ID }} - secrets: | - secret/data/common/github.com/actions/camunda-cloud/zeebe-process-test REGISTRY_HUB_DOCKER_COM_USR; - secret/data/common/github.com/actions/camunda-cloud/zeebe-process-test REGISTRY_HUB_DOCKER_COM_PSW; - - - name: Set up Java environment - uses: actions/setup-java@v2.5.0 - with: - distribution: temurin - java-version: 11 - - - name: Build jar - run: | - cd engine-agent - mvn clean package -DskipTests - - - name: Build Docker image - run: | - if ! [ -z "${{ github.event.release.tag_name }}" ] && ! grep -q "$TAG" <<< "SNAPSHOT"; then - TAG="${{ github.event.release.tag_name }}" - fi - cd engine-agent - docker build . -t $IMAGE_NAME:$TAG - - - name: Push Docker image - run: | - echo '${{ steps.secrets.outputs.REGISTRY_HUB_DOCKER_COM_PSW }}' | docker login -u '${{ steps.secrets.outputs.REGISTRY_HUB_DOCKER_COM_USR }}' --password-stdin - echo $IMAGE_NAME - echo $TAG - IMAGE_ID=$OWNER/$IMAGE_NAME - echo $IMAGE_ID - docker tag $IMAGE_NAME $IMAGE_ID:$TAG - docker push $IMAGE_ID:$TAG diff --git a/assertions/src/main/java/io/camunda/zeebe/process/test/assertions/ProcessInstanceAssert.java b/assertions/src/main/java/io/camunda/zeebe/process/test/assertions/ProcessInstanceAssert.java index c23cdbfc9..62e8e1a2b 100644 --- a/assertions/src/main/java/io/camunda/zeebe/process/test/assertions/ProcessInstanceAssert.java +++ b/assertions/src/main/java/io/camunda/zeebe/process/test/assertions/ProcessInstanceAssert.java @@ -194,7 +194,9 @@ public ProcessInstanceAssert hasPassedElement(final String elementId, final int .count(); assertThat(count) - .withFailMessage("Expected element with id %s to be passed %s times", elementId, times) + .withFailMessage( + "Expected element with id %s to be passed %s times, but was %s", + elementId, times, count) .isEqualTo(times); return this; diff --git a/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/AgentProperties.java b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/AgentProperties.java new file mode 100644 index 000000000..d331548d7 --- /dev/null +++ b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/AgentProperties.java @@ -0,0 +1,46 @@ +package io.camunda.zeebe.process.test.engine.agent; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AgentProperties { + + private static final Logger LOG = LoggerFactory.getLogger(AgentProperties.class); + + private static final String PROPERTIES_FILE = "/config.properties"; + public static final String GATEWAY_PORT = "gateway.port"; + public static final String CONTROLLER_PORT = "controller.port"; + private static final Properties PROPERTIES = new Properties(); + + static { + try (final InputStream inputStream = + AgentProperties.class.getResourceAsStream(PROPERTIES_FILE)) { + PROPERTIES.load(inputStream); + } catch (NullPointerException e) { + LOG.error( + "Could not find property file with name " + + PROPERTIES_FILE + + ". Please make sure this property file is available in the resources folder.", + e); + throw new RuntimeException(e); + } catch (IOException e) { + LOG.error("Could not read properties from file", e); + throw new RuntimeException(e); + } + } + + public static int getControllerPort() { + return Integer.parseInt(getProperty(CONTROLLER_PORT)); + } + + public static int getGatewayPort() { + return Integer.parseInt(getProperty(GATEWAY_PORT)); + } + + private static String getProperty(final String property) { + return PROPERTIES.getProperty(property); + } +} diff --git a/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/EngineControlImpl.java b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/EngineControlImpl.java index 262731583..4ca86903e 100644 --- a/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/EngineControlImpl.java +++ b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/EngineControlImpl.java @@ -1,7 +1,5 @@ package io.camunda.zeebe.process.test.engine.agent; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.ObjectMapper; import io.camunda.zeebe.process.test.api.InMemoryEngine; import io.camunda.zeebe.process.test.engine.EngineFactory; import io.camunda.zeebe.process.test.engine.protocol.EngineControlGrpc.EngineControlImplBase; @@ -17,21 +15,22 @@ import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.StopEngineResponse; import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.WaitForIdleStateRequest; import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.WaitForIdleStateResponse; -import io.camunda.zeebe.protocol.record.Record; -import io.grpc.Status; import io.grpc.stub.StreamObserver; import java.time.Duration; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.util.List; public final class EngineControlImpl extends EngineControlImplBase { private static final Logger LOG = LoggerFactory.getLogger(EngineControlImpl.class); private InMemoryEngine engine; + private RecordStreamSourceWrapper recordStreamSource; public EngineControlImpl(final InMemoryEngine engine) { this.engine = engine; + this.recordStreamSource = new RecordStreamSourceWrapper(engine.getRecordStreamSource()); } @Override @@ -60,7 +59,8 @@ public void resetEngine( final ResetEngineRequest request, final StreamObserver responseObserver) { engine.stop(); - engine = EngineFactory.create(); + engine = EngineFactory.create(AgentProperties.getGatewayPort()); + recordStreamSource = new RecordStreamSourceWrapper(engine.getRecordStreamSource()); engine.start(); final ResetEngineResponse response = ResetEngineResponse.newBuilder().build(); @@ -91,22 +91,12 @@ public void waitForIdleState( @Override public void getRecords( final GetRecordsRequest request, final StreamObserver responseObserver) { - final ObjectMapper mapper = new ObjectMapper(); - final Iterable> records = engine.getRecordStreamSource().records(); - for (final Record record : records) { - try { - final String recordJson = mapper.writeValueAsString(record); - final RecordResponse response = - RecordResponse.newBuilder().setRecordJson(recordJson).build(); - responseObserver.onNext(response); - } catch (JsonProcessingException e) { - final String errorMessage = String.format("Failed mapping record %d at position %d to JSON", - record.getKey(), record.getPosition()); - LOG.error(errorMessage, e); - responseObserver.onError(Status.INTERNAL.asException()); - return; - } - } + final List mappedRecords = recordStreamSource.getMappedRecords(); + + mappedRecords.forEach( + record -> + responseObserver.onNext(RecordResponse.newBuilder().setRecordJson(record).build())); + responseObserver.onCompleted(); } } diff --git a/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/RecordStreamSourceWrapper.java b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/RecordStreamSourceWrapper.java new file mode 100644 index 000000000..3e7e9df24 --- /dev/null +++ b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/RecordStreamSourceWrapper.java @@ -0,0 +1,23 @@ +package io.camunda.zeebe.process.test.engine.agent; + +import io.camunda.zeebe.process.test.api.RecordStreamSource; +import java.util.ArrayList; +import java.util.List; + +public class RecordStreamSourceWrapper { + + private final List mappedRecords = new ArrayList<>(); + private final RecordStreamSource recordStreamSource; + + public RecordStreamSourceWrapper(final RecordStreamSource recordStreamSource) { + this.recordStreamSource = recordStreamSource; + } + + public List getMappedRecords() { + synchronized (mappedRecords) { + mappedRecords.clear(); + recordStreamSource.records().forEach(record -> mappedRecords.add(record.toJson())); + } + return new ArrayList<>(mappedRecords); + } +} diff --git a/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/ZeebeProcessTestEngine.java b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/ZeebeProcessTestEngine.java index 716ba5702..a715332e4 100644 --- a/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/ZeebeProcessTestEngine.java +++ b/engine-agent/src/main/java/io/camunda/zeebe/process/test/engine/agent/ZeebeProcessTestEngine.java @@ -9,11 +9,14 @@ public class ZeebeProcessTestEngine { public static void main(String[] args) throws IOException { - final InMemoryEngine engine = EngineFactory.create(); + final InMemoryEngine engine = EngineFactory.create(AgentProperties.getGatewayPort()); final EngineControlImpl engineService = new EngineControlImpl(engine); + final Server server = + ServerBuilder.forPort(AgentProperties.getControllerPort()) + .addService(engineService) + .build(); - final Server server = ServerBuilder.forPort(26501).addService(engineService).build(); - + engine.start(); server.start(); } } diff --git a/engine-agent/src/main/resources/config.properties b/engine-agent/src/main/resources/config.properties new file mode 100644 index 000000000..159705358 --- /dev/null +++ b/engine-agent/src/main/resources/config.properties @@ -0,0 +1,2 @@ +gateway.port=26500 +controller.port=26501 diff --git a/engine/src/main/java/io/camunda/zeebe/process/test/engine/EngineFactory.java b/engine/src/main/java/io/camunda/zeebe/process/test/engine/EngineFactory.java index 7f6ad80b8..64c85ecee 100644 --- a/engine/src/main/java/io/camunda/zeebe/process/test/engine/EngineFactory.java +++ b/engine/src/main/java/io/camunda/zeebe/process/test/engine/EngineFactory.java @@ -25,9 +25,12 @@ public class EngineFactory { public static InMemoryEngine create() { + return create(26500); + } + + public static InMemoryEngine create(final int port) { final int partitionId = 1; final int partitionCount = 1; - final int port = 26500; final ControlledActorClock clock = createActorClock(); final ActorScheduler scheduler = createAndStartActorScheduler(clock); diff --git a/engine/src/test/java/io/camunda/zeebe/process/test/engine/EngineClientTest.java b/engine/src/test/java/io/camunda/zeebe/process/test/engine/EngineClientTest.java index 34b207ba8..b3af3c045 100644 --- a/engine/src/test/java/io/camunda/zeebe/process/test/engine/EngineClientTest.java +++ b/engine/src/test/java/io/camunda/zeebe/process/test/engine/EngineClientTest.java @@ -51,7 +51,7 @@ class EngineClientTest { @BeforeEach public void setupGrpcServer() { - zeebeEngine = new EngineFactory().create(); + zeebeEngine = EngineFactory.create(); zeebeEngine.start(); zeebeClient = zeebeEngine.createClient(); } diff --git a/extension-testcontainer/pom.xml b/extension-testcontainer/pom.xml new file mode 100644 index 000000000..504ae129c --- /dev/null +++ b/extension-testcontainer/pom.xml @@ -0,0 +1,65 @@ + + + 4.0.0 + + + io.camunda + zeebe-process-test-root + 1.3.4-SNAPSHOT + ../pom.xml + + + zeebe-process-test-extension-testcontainer + 1.3.4-SNAPSHOT + jar + + Zeebe Process Test Extension Testcontainer + + + 8 + + + + + io.camunda + zeebe-process-test-assertions + + + + io.camunda + zeebe-process-test-engine-protocol + + + + io.camunda + zeebe-client-java + + + + io.camunda + zeebe-protocol-jackson + + + + org.junit.jupiter + junit-jupiter-api + + + + org.testcontainers + testcontainers + + + + org.testcontainers + junit-jupiter + + + + org.mockito + mockito-core + test + + + + diff --git a/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ContainerProperties.java b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ContainerProperties.java new file mode 100644 index 000000000..cb29126e4 --- /dev/null +++ b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ContainerProperties.java @@ -0,0 +1,49 @@ +package io.camunda.zeebe.process.test.extension.testcontainer; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public final class ContainerProperties { + + private static final Logger LOG = LoggerFactory.getLogger("io.camunda.zeebe-process-test"); + + private static final String PROPERTIES_FILE = "/config.properties"; + public static final String IMAGE_NAME = "container.image.name"; + public static final String IMAGE_TAG = "container.image.tag"; + public static final String GATEWAY_PORT = "container.gateway.port"; + public static final String PORT = "container.port"; + private static final Properties PROPERTIES = new Properties(); + + static { + try (final InputStream inputStream = + ContainerProperties.class.getResourceAsStream(PROPERTIES_FILE)) { + PROPERTIES.load(inputStream); + } catch (FileNotFoundException e) { + LOG.error("Could not find property file with name " + PROPERTIES_FILE, e); + throw new RuntimeException(e); + } catch (IOException e) { + LOG.error("Could not read properties from file", e); + throw new RuntimeException(e); + } + } + + public static String getDockerImageName() { + return getProperty(IMAGE_NAME) + ":" + getProperty(IMAGE_TAG); + } + + public static int getContainerPort() { + return Integer.parseInt(getProperty(PORT)); + } + + public static int getGatewayPort() { + return Integer.parseInt(getProperty(GATEWAY_PORT)); + } + + private static String getProperty(final String property) { + return PROPERTIES.getProperty(property); + } +} diff --git a/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ContainerizedEngine.java b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ContainerizedEngine.java new file mode 100644 index 000000000..11f6c0abc --- /dev/null +++ b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ContainerizedEngine.java @@ -0,0 +1,158 @@ +package io.camunda.zeebe.process.test.extension.testcontainer; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.api.RecordStreamSource; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlGrpc; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlGrpc.EngineControlBlockingStub; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.GetRecordsRequest; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.IncreaseTimeRequest; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.RecordResponse; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.ResetEngineRequest; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.StartEngineRequest; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.StopEngineRequest; +import io.camunda.zeebe.process.test.engine.protocol.EngineControlOuterClass.WaitForIdleStateRequest; +import io.camunda.zeebe.protocol.jackson.record.AbstractRecord; +import io.camunda.zeebe.protocol.record.Record; +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class ContainerizedEngine implements InMemoryEngine { + + private final String host; + private final int containerPort; + private final int channelPort; + + public ContainerizedEngine(final String host, final int containerPort, final int channelPort) { + this.host = host; + this.containerPort = containerPort; + this.channelPort = channelPort; + } + + @Override + public void start() { + final ManagedChannel channel = getChannel(); + final EngineControlBlockingStub stub = getStub(channel); + + final StartEngineRequest request = StartEngineRequest.newBuilder().build(); + stub.startEngine(request); + + closeChannel(channel); + } + + @Override + public void stop() { + final ManagedChannel channel = getChannel(); + final EngineControlBlockingStub stub = getStub(channel); + + final StopEngineRequest request = StopEngineRequest.newBuilder().build(); + stub.stopEngine(request); + + closeChannel(channel); + } + + public void reset() { + final ManagedChannel channel = getChannel(); + final EngineControlBlockingStub stub = getStub(channel); + + final ResetEngineRequest request = ResetEngineRequest.newBuilder().build(); + stub.resetEngine(request); + + closeChannel(channel); + } + + @Override + public RecordStreamSource getRecordStreamSource() { + return new RecordStreamSourceImpl(this, getRecords()); + } + + public List> getRecords() { + final ManagedChannel channel = getChannel(); + final EngineControlBlockingStub stub = getStub(channel); + final ObjectMapper mapper = new ObjectMapper(); + final List> mappedRecords = new ArrayList<>(); + + final GetRecordsRequest request = GetRecordsRequest.newBuilder().build(); + final Iterator response = stub.getRecords(request); + + while (response.hasNext()) { + final RecordResponse recordResponse = response.next(); + try { + final Record record = + mapper.readValue(recordResponse.getRecordJson(), AbstractRecord.class); + mappedRecords.add(record); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + closeChannel(channel); + return mappedRecords; + } + + @Override + public ZeebeClient createClient() { + return ZeebeClient.newClientBuilder() + .gatewayAddress(getGatewayAddress()) + .usePlaintext() + .build(); + } + + @Override + public String getGatewayAddress() { + return host + ":" + channelPort; + } + + @Override + public void increaseTime(final Duration timeToAdd) { + final ManagedChannel channel = getChannel(); + final EngineControlBlockingStub stub = getStub(channel); + + final IncreaseTimeRequest request = + IncreaseTimeRequest.newBuilder().setMilliseconds((int) timeToAdd.toMillis()).build(); + stub.increaseTime(request); + + closeChannel(channel); + } + + @Override + public void runOnIdleState(final Runnable callback) { + // TODO remove this from interface? + } + + @Override + public void waitForIdleState() { + final ManagedChannel channel = getChannel(); + final EngineControlBlockingStub stub = getStub(channel); + + final WaitForIdleStateRequest request = + WaitForIdleStateRequest.newBuilder().setTimeout(1000).build(); + stub.waitForIdleState(request); + + closeChannel(channel); + } + + private ManagedChannel getChannel() { + return ManagedChannelBuilder.forAddress(host, containerPort).usePlaintext().build(); + } + + private EngineControlBlockingStub getStub(final ManagedChannel channel) { + return EngineControlGrpc.newBlockingStub(channel); + } + + private void closeChannel(final ManagedChannel channel) { + channel.shutdown(); + try { + channel.awaitTermination(100, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/EngineContainer.java b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/EngineContainer.java new file mode 100644 index 000000000..9c4689473 --- /dev/null +++ b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/EngineContainer.java @@ -0,0 +1,33 @@ +package io.camunda.zeebe.process.test.extension.testcontainer; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.output.Slf4jLogConsumer; +import org.testcontainers.utility.DockerImageName; + +public final class EngineContainer extends GenericContainer { + + private static final Logger LOG = LoggerFactory.getLogger("io.camunda.zeebe-process-test"); + + private static EngineContainer INSTANCE; + + private EngineContainer(final String imageName) { + super(DockerImageName.parse(imageName)); + } + + public static EngineContainer getContainer() { + if (INSTANCE == null) { + createContainer(); + } + return INSTANCE; + } + + private static void createContainer() { + INSTANCE = + new EngineContainer(ContainerProperties.getDockerImageName()) + .withExposedPorts( + ContainerProperties.getContainerPort(), ContainerProperties.getGatewayPort()) + .withLogConsumer(new Slf4jLogConsumer(LOG)); + } +} diff --git a/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/RecordStreamSourceImpl.java b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/RecordStreamSourceImpl.java new file mode 100644 index 000000000..1dbcb6819 --- /dev/null +++ b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/RecordStreamSourceImpl.java @@ -0,0 +1,31 @@ +package io.camunda.zeebe.process.test.extension.testcontainer; + +import io.camunda.zeebe.process.test.api.RecordStreamSource; +import io.camunda.zeebe.protocol.record.Record; +import java.util.ArrayList; + +public class RecordStreamSourceImpl implements RecordStreamSource { + + private final ContainerizedEngine engine; + private Iterable> records; + + public RecordStreamSourceImpl(final ContainerizedEngine engine) { + this(engine, new ArrayList<>()); + } + + public RecordStreamSourceImpl( + final ContainerizedEngine engine, final Iterable> records) { + this.engine = engine; + this.records = records; + } + + @Override + public Iterable> records() { + updateWithNewRecords(); + return records; + } + + private void updateWithNewRecords() { + records = engine.getRecords(); + } +} diff --git a/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ZeebeProcessTest.java b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ZeebeProcessTest.java new file mode 100644 index 000000000..cb85313f8 --- /dev/null +++ b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ZeebeProcessTest.java @@ -0,0 +1,42 @@ +package io.camunda.zeebe.process.test.extension.testcontainer; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.junit.jupiter.api.extension.ExtendWith; + +/** + * This annotation can be used to test BPMN processes. It will make use of testcontainers to run an + * in memory engine. To use this annotation Java 8 or higher is required. Docker also needs to be + * running. + * + * Annotating test classes with this annotation will do a couple of things: + * + *
    + *
  • It start a docker container running and in memory engine. + *
  • It will create a client which can be used to interact with the engine. + *
  • It will (optionally) inject 3 fields in your test class: + *
      + *
    • InMemoryEngine - This is the engine that will run your process. It will provide some + * basic functionality to help you write your tests, such as waiting for an idle state + * and increasing the time. + *
    • ZeebeClient - This is the client that allows you to communicate with the engine. It + * allows you to send commands to the engine. + *
    • RecordStream - This gives you access to all the records that are processed by + * the engine. It is what the assertions use to verify expectations. This grants you the + * freedom to create your own assertions. + *
    + *
  • It will take care of cleaning up the engine and client when the testcase is finished. + *
+ * + * @since Java 8 + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Inherited +@ExtendWith(ZeebeProcessTestExtension.class) +public @interface ZeebeProcessTest { + +} diff --git a/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ZeebeProcessTestExtension.java b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ZeebeProcessTestExtension.java new file mode 100644 index 000000000..120756322 --- /dev/null +++ b/extension-testcontainer/src/main/java/io.camunda.zeebe.process.test.extension.testcontainer/ZeebeProcessTestExtension.java @@ -0,0 +1,105 @@ +package io.camunda.zeebe.process.test.extension.testcontainer; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.filters.RecordStream; +import java.lang.reflect.Field; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.TestWatcher; +import org.junit.platform.commons.util.ReflectionUtils; + +public class ZeebeProcessTestExtension + implements BeforeEachCallback, AfterEachCallback, BeforeAllCallback, TestWatcher { + + private static final String KEY_ZEEBE_CLIENT = "ZEEBE_CLIENT"; + private static final String KEY_ZEEBE_ENGINE = "ZEEBE_ENGINE"; + + @Override + public void beforeAll(final ExtensionContext extensionContext) { + final EngineContainer container = EngineContainer.getContainer(); + container.start(); + + final ContainerizedEngine engine = + new ContainerizedEngine( + container.getHost(), + container.getMappedPort(ContainerProperties.getContainerPort()), + container.getMappedPort(ContainerProperties.getGatewayPort())); + + getStore(extensionContext).put(KEY_ZEEBE_ENGINE, engine); + } + + @Override + public void beforeEach(final ExtensionContext extensionContext) { + final Object engineContent = getStore(extensionContext.getParent().get()).get(KEY_ZEEBE_ENGINE); + final ContainerizedEngine engine = (ContainerizedEngine) engineContent; + engine.reset(); + + final ZeebeClient client = engine.createClient(); + final RecordStream recordStream = RecordStream.of(new RecordStreamSourceImpl(engine)); + BpmnAssert.initRecordStream(recordStream); + + getStore(extensionContext).put(KEY_ZEEBE_CLIENT, client); + injectFields(extensionContext, engine, client, recordStream); + } + + @Override + public void afterEach(final ExtensionContext extensionContext) { + final Object clientContent = getStore(extensionContext).get(KEY_ZEEBE_CLIENT); + final ZeebeClient client = (ZeebeClient) clientContent; + client.close(); + } + + @Override + public void testFailed(final ExtensionContext extensionContext, final Throwable cause) { + final Object engineContent = getStore(extensionContext.getParent().get()).get(KEY_ZEEBE_ENGINE); + final ContainerizedEngine engine = (ContainerizedEngine) engineContent; + RecordStream.of(engine.getRecordStreamSource()).print(true); + } + + private void injectFields(final ExtensionContext extensionContext, final Object... objects) { + final Class requiredTestClass = extensionContext.getRequiredTestClass(); + final Field[] declaredFields = requiredTestClass.getDeclaredFields(); + for (Object object : objects) { + final Optional field = getField(declaredFields, object); + field.ifPresent(value -> injectField(extensionContext, value, object)); + } + } + + private Optional getField(final Field[] declaredFields, final Object object) { + final List fields = + Arrays.stream(declaredFields) + .filter(field -> field.getType().isInstance(object)) + .collect(Collectors.toList()); + + if (fields.size() > 1) { + throw new IllegalStateException( + String.format( + "Expected at most one field of type %s, but " + + "found %s. Please make sure at most one field of type %s has been declared in the" + + " test class.", + object.getClass().getSimpleName(), fields.size(), object.getClass().getSimpleName())); + } + return fields.size() == 0 ? Optional.empty() : Optional.of(fields.get(0)); + } + + private void injectField( + final ExtensionContext extensionContext, final Field field, final Object object) { + try { + ReflectionUtils.makeAccessible(field); + field.set(extensionContext.getRequiredTestInstance(), object); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + private ExtensionContext.Store getStore(final ExtensionContext context) { + return context.getStore(ExtensionContext.Namespace.create(getClass(), context.getUniqueId())); + } +} diff --git a/extension-testcontainer/src/main/resources/config.properties b/extension-testcontainer/src/main/resources/config.properties new file mode 100644 index 000000000..3fb01f413 --- /dev/null +++ b/extension-testcontainer/src/main/resources/config.properties @@ -0,0 +1,8 @@ +# The image name of the containerized engine +container.image.name=camunda/zeebe-process-test-engine +# The tag of the image +container.image.tag=latest +# The port at which the in memory engine gateway is listening +container.gateway.port=26500 +# The port at which the engine controller is listening +container.port=26501 diff --git a/extension/pom.xml b/extension/pom.xml index c02a93959..a9b9566a2 100644 --- a/extension/pom.xml +++ b/extension/pom.xml @@ -13,6 +13,8 @@ 1.3.4-SNAPSHOT jar + Zeebe Process Test Extension + io.camunda diff --git a/extension/src/main/java/io/camunda/zeebe/process/test/extension/ZeebeProcessTest.java b/extension/src/main/java/io/camunda/zeebe/process/test/extension/ZeebeProcessTest.java index 4644f8bcf..821a5dc3d 100644 --- a/extension/src/main/java/io/camunda/zeebe/process/test/extension/ZeebeProcessTest.java +++ b/extension/src/main/java/io/camunda/zeebe/process/test/extension/ZeebeProcessTest.java @@ -7,6 +7,31 @@ import java.lang.annotation.Target; import org.junit.jupiter.api.extension.ExtendWith; +/** + * This annotation can be used to test BPMN processes. It will run an in memory Zeebe engine. To use + * this annotation Java 17 or higher is required. + * + * Annotating test classes with this annotation will do a couple of things: + * + *
    + *
  • It will create and start an in memory engine. This will be a new engine for each testcase. + *
  • It will create a client which can be used to interact with the engine. + *
  • It will (optionally) inject 3 fields in your test class: + *
      + *
    • InMemoryEngine - This is the engine that will run your process. It will provide some + * basic functionality to help you write your tests, such as waiting for an idle state + * and increasing the time. + *
    • ZeebeClient - This is the client that allows you to communicate with the engine. It + * allows you to send commands to the engine. + *
    • RecordStream - This gives you access to all the records that are processed by + * the engine. It is what the assertions use to verify expectations. This grants you the + * freedom to create your own assertions. + *
    + *
  • It will take care of cleaning up the engine and client when the testcase is finished. + *
+ * + * @since Java 17 + */ @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Inherited diff --git a/pom.xml b/pom.xml index fffdf77cd..c2fbd6aae 100644 --- a/pom.xml +++ b/pom.xml @@ -22,6 +22,7 @@ engine-agent engine-protocol extension + extension-testcontainer filters qa @@ -40,6 +41,7 @@ 0.6.0 3.0.2 31.0.1-jre + 2.8.9-ea-1 2.13.1 1.3.2 5.10.0 @@ -102,6 +104,12 @@ 1.3.4-SNAPSHOT
+ + io.camunda + zeebe-process-test-extension-testcontainer + 1.3.4-SNAPSHOT + + io.camunda zeebe-process-test-filters @@ -156,6 +164,18 @@ ${dependency.javax.version} + + org.testcontainers + testcontainers + ${dependency.testcontainers.version} + + + + org.testcontainers + junit-jupiter + ${dependency.testcontainers.version} + + com.google.protobuf @@ -233,6 +253,11 @@ log4j-slf4j-impl ${dependency.log4j.version} + + org.immutables + value + ${dependency.immutables.version} + diff --git a/qa/pom.xml b/qa/pom.xml index 308df519f..5cc4dfbdd 100644 --- a/qa/pom.xml +++ b/qa/pom.xml @@ -21,6 +21,12 @@ test + + io.camunda + zeebe-process-test-extension-testcontainer + test + + org.junit.jupiter junit-jupiter-api @@ -40,4 +46,42 @@
+ + + regular + + + + org.apache.maven.plugins + maven-surefire-plugin + ${plugin.version.surefire} + + + **/testcontainer/**/*.java + + + + + + + + + testcontainer + + + + org.apache.maven.plugins + maven-surefire-plugin + ${plugin.version.surefire} + + + **/regular/**/*.java + + + + + + + + diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/DeploymentAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/DeploymentAssertTest.java similarity index 98% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/DeploymentAssertTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/DeploymentAssertTest.java index 34bef4921..ccd86e78e 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/DeploymentAssertTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/DeploymentAssertTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.assertions; +package io.camunda.zeebe.process.test.qa.regular.assertions; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/IncidentAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/IncidentAssertTest.java similarity index 99% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/IncidentAssertTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/IncidentAssertTest.java index 6729233ff..199dfeb2d 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/IncidentAssertTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/IncidentAssertTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.assertions; +package io.camunda.zeebe.process.test.qa.regular.assertions; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/JobAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/JobAssertTest.java similarity index 99% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/JobAssertTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/JobAssertTest.java index 4a03e042d..5f4abf31f 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/JobAssertTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/JobAssertTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.assertions; +package io.camunda.zeebe.process.test.qa.regular.assertions; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.entry; @@ -75,7 +75,7 @@ void testHasDeadline() { // then final ActivatedJob actual = jobActivationResponse.getJobs().get(0); - BpmnAssert.assertThat(actual).hasDeadline(expectedDeadline, offset(20L)); + BpmnAssert.assertThat(actual).hasDeadline(expectedDeadline, offset(50L)); } @Test diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/MessageAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/MessageAssertTest.java similarity index 99% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/MessageAssertTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/MessageAssertTest.java index 858d0174d..69178f7fc 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/MessageAssertTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/MessageAssertTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.assertions; +package io.camunda.zeebe.process.test.qa.regular.assertions; import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/ProcessAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/ProcessAssertTest.java similarity index 99% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/ProcessAssertTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/ProcessAssertTest.java index 1d6ffc8fa..3367840a9 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/ProcessAssertTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/ProcessAssertTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.assertions; +package io.camunda.zeebe.process.test.qa.regular.assertions; import static org.assertj.core.api.Assertions.assertThatThrownBy; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/ProcessInstanceAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/ProcessInstanceAssertTest.java similarity index 99% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/ProcessInstanceAssertTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/ProcessInstanceAssertTest.java index e412caced..d0d137520 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/assertions/ProcessInstanceAssertTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/assertions/ProcessInstanceAssertTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.assertions; +package io.camunda.zeebe.process.test.qa.regular.assertions; import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.mock; @@ -647,7 +647,7 @@ public void testProcessInstanceHasPassedElementFailure() { .hasPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID)) .isInstanceOf(AssertionError.class) .hasMessage( - "Expected element with id %s to be passed 1 times", + "Expected element with id %s to be passed 1 times, but was 0", ProcessPackLoopingServiceTask.ELEMENT_ID); } @@ -672,7 +672,7 @@ public void testProcessInstanceHasNotPassedElementFailure() { .hasNotPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID)) .isInstanceOf(AssertionError.class) .hasMessage( - "Expected element with id %s to be passed 0 times", + "Expected element with id %s to be passed 0 times, but was 1", ProcessPackLoopingServiceTask.ELEMENT_ID); } diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/inspections/ProcessEventInspectionsTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/inspections/ProcessEventInspectionsTest.java similarity index 98% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/inspections/ProcessEventInspectionsTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/inspections/ProcessEventInspectionsTest.java index 545ca608c..946a25a06 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/inspections/ProcessEventInspectionsTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/inspections/ProcessEventInspectionsTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.inspections; +package io.camunda.zeebe.process.test.qa.regular.inspections; import io.camunda.zeebe.client.ZeebeClient; import io.camunda.zeebe.client.api.response.DeploymentEvent; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/inspections/ProcessInstanceInspectionsTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/inspections/ProcessInstanceInspectionsTest.java similarity index 97% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/inspections/ProcessInstanceInspectionsTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/inspections/ProcessInstanceInspectionsTest.java index 474753c04..9c53adb4d 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/inspections/ProcessInstanceInspectionsTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/inspections/ProcessInstanceInspectionsTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.inspections; +package io.camunda.zeebe.process.test.qa.regular.inspections; import io.camunda.zeebe.client.ZeebeClient; import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/multithread/MultiThreadTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/multithread/MultiThreadTest.java similarity index 97% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/multithread/MultiThreadTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/multithread/MultiThreadTest.java index 283ecaebb..f3f2ec2c2 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/multithread/MultiThreadTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/multithread/MultiThreadTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.multithread; +package io.camunda.zeebe.process.test.qa.regular.multithread; import static io.camunda.zeebe.process.test.assertions.BpmnAssert.assertThat; import static io.camunda.zeebe.process.test.qa.util.Utilities.deployProcess; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/multithread/WorkerTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/multithread/WorkerTest.java similarity index 96% rename from qa/src/test/java/io/camunda/zeebe/process/test/qa/multithread/WorkerTest.java rename to qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/multithread/WorkerTest.java index b36624fe4..ebbe20953 100644 --- a/qa/src/test/java/io/camunda/zeebe/process/test/qa/multithread/WorkerTest.java +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/regular/multithread/WorkerTest.java @@ -1,4 +1,4 @@ -package io.camunda.zeebe.process.test.qa.multithread; +package io.camunda.zeebe.process.test.qa.regular.multithread; import io.camunda.zeebe.client.ZeebeClient; import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/DeploymentAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/DeploymentAssertTest.java new file mode 100644 index 000000000..e5f32466d --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/DeploymentAssertTest.java @@ -0,0 +1,157 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.assertions; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.DeploymentEvent; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.assertions.ProcessAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackLoopingServiceTask; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackMultipleTasks; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class DeploymentAssertTest { + + public static final String WRONG_VALUE = "wrong value"; + + // These tests are for testing assertions as well as examples for users + @Nested + class HappyPathTests { + + private ZeebeClient client; + + @Test + public void testContainsProcessesById() { + // when + final DeploymentEvent deploymentEvent = + Utilities.deployProcesses( + client, + ProcessPackLoopingServiceTask.RESOURCE_NAME, + ProcessPackMultipleTasks.RESOURCE_NAME); + + // then + BpmnAssert.assertThat(deploymentEvent) + .containsProcessesByBpmnProcessId( + ProcessPackLoopingServiceTask.PROCESS_ID, ProcessPackMultipleTasks.PROCESS_ID); + } + + @Test + public void testContainsProcessesByResourceName() { + // when + final DeploymentEvent deploymentEvent = + Utilities.deployProcesses( + client, + ProcessPackLoopingServiceTask.RESOURCE_NAME, + ProcessPackMultipleTasks.RESOURCE_NAME); + + // then + BpmnAssert.assertThat(deploymentEvent) + .containsProcessesByResourceName( + ProcessPackLoopingServiceTask.RESOURCE_NAME, ProcessPackMultipleTasks.RESOURCE_NAME); + } + + @Test + public void testExtractingProcessByBpmnProcessId() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + Assertions.assertThat(processAssert).isNotNull(); + } + + @Test + public void testExtractingProcessByResourceName() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByResourceName(ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // then + Assertions.assertThat(processAssert).isNotNull(); + } + } + + // These tests are just for assertion testing purposes. These should not be used as examples. + @Nested + class UnhappyPathTests { + + private ZeebeClient client; + + @Test + public void testContainsProcessesByIdFailure() { + // when + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(deploymentEvent) + .containsProcessesByBpmnProcessId(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll(WRONG_VALUE, ProcessPackLoopingServiceTask.PROCESS_ID); + } + + @Test + public void testContainsProcessesByResourceNameFailure() { + // when + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(deploymentEvent) + .containsProcessesByResourceName(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll(WRONG_VALUE, ProcessPackLoopingServiceTask.PROCESS_ID); + } + + @Test + public void testExtractingProcessByBpmnProcessIdFailure() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected to find one process for BPMN process id 'wrong value' but found 0: []"); + } + + @Test + public void testExtractingProcessByResourceName() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByResourceName(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected to find one process for resource name 'wrong value' but found 0: []"); + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/IncidentAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/IncidentAssertTest.java new file mode 100644 index 000000000..c12ed68df --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/IncidentAssertTest.java @@ -0,0 +1,398 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.assertions; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ActivateJobsResponse; +import io.camunda.zeebe.client.api.response.ActivatedJob; +import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.assertions.IncidentAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackLoopingServiceTask; +import io.camunda.zeebe.protocol.record.value.ErrorType; +import java.util.Collections; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.StringAssert; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class IncidentAssertTest { + + public static final String WRONG_VALUE = "wrong value"; + public static final String ERROR_CODE = "error"; + public static final String ERROR_MESSAGE = "error occurred"; + + // These tests are for testing assertions as well as examples for users + @Nested + class HappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + void testHasErrorType() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + incidentAssert.hasErrorType(ErrorType.UNHANDLED_ERROR_EVENT); + } + + @Test + void testHasErrorMessage() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + incidentAssert.hasErrorMessage( + "Expected to throw an error event with the code 'error' with message 'error occurred', but it was not caught. No error events are available in the scope."); + } + + @Test + void testExtractErrorMessage() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + final StringAssert messageAssert = incidentAssert.extractErrorMessage(); + + // then + Assertions.assertThat(messageAssert).isNotNull(); + messageAssert.isEqualTo( + "Expected to throw an error event with the code 'error' with message 'error occurred', but it was not caught. No error events are available in the scope."); + } + + @Test + void testWasRaisedInProcessInstance() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent processInstanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + incidentAssert.wasRaisedInProcessInstance(processInstanceEvent); + incidentAssert.wasRaisedInProcessInstance(processInstanceEvent.getProcessInstanceKey()); + } + + @Test + void testOccurredOnElement() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + final Map variables = + Collections.singletonMap( + ProcessPackLoopingServiceTask.TOTAL_LOOPS, "invalid value"); // will cause incident + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + /* will raise an incident in the gateway because VAR_TOTAL_LOOPS is a string, but needs to be an int */ + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + final ActivatedJob job = jobActivationResponse.getJobs().get(0); + client.newCompleteCommand(job.getKey()).send().join(); + + engine.waitForIdleState(); + + final IncidentAssert incidentAssert = + BpmnAssert.assertThat(instanceEvent).extractLatestIncident(); + + // then + incidentAssert.occurredOnElement(ProcessPackLoopingServiceTask.GATEWAY_ELEMENT_ID); + } + + @Test + void testOccurredDuringJob() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob job = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, job.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(job).extractLatestIncident(); + + // then + incidentAssert.occurredDuringJob(job); + incidentAssert.occurredDuringJob(job.getKey()); + } + + @Test + void testIsResolved() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + final long incidentKey = incidentAssert.getIncidentKey(); + client.newResolveIncidentCommand(incidentKey).send().join(); + + Utilities.waitForIdleState(engine); + + // then + incidentAssert.isResolved(); + } + + @Test + void testIsUnresolved() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + incidentAssert.isUnresolved(); + } + } + + // These tests are just for assertion testing purposes. These should not be used as examples. + @Nested + class UnhappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + void testHasErrorTypeFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + assertThatThrownBy(() -> incidentAssert.hasErrorType(ErrorType.IO_MAPPING_ERROR)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith( + "Error type was not 'IO_MAPPING_ERROR' but was 'UNHANDLED_ERROR_EVENT' instead"); + } + + @Test + void testHasErrorMessageFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + assertThatThrownBy(() -> incidentAssert.hasErrorMessage(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith( + "Error message was not 'wrong value' but was 'Expected to throw an error event with the code 'error' with message 'error occurred', but it was not caught. No error events are available in the scope.' instead"); + } + + @Test + void testWasRaisedInProcessInstanceFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent processInstanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + assertThatThrownBy(() -> incidentAssert.wasRaisedInProcessInstance(-1)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith( + "Incident was not raised in process instance -1 but was raised in %d instead", + processInstanceEvent.getProcessInstanceKey()); + } + + @Test + void testOccurredOnElementFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + final Map variables = + Collections.singletonMap( + ProcessPackLoopingServiceTask.TOTAL_LOOPS, "invalid value"); // will cause incident + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + /* will raise an incident in the gateway because VAR_TOTAL_LOOPS is a string, but needs to be an int */ + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + final ActivatedJob job = jobActivationResponse.getJobs().get(0); + client.newCompleteCommand(job.getKey()).send().join(); + + engine.waitForIdleState(); + + final IncidentAssert incidentAssert = + BpmnAssert.assertThat(instanceEvent).extractLatestIncident(); + + // then + assertThatThrownBy(() -> incidentAssert.occurredOnElement(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith( + "Error type was not raised on element '%s' but was raised on '%s' instead", + WRONG_VALUE, ProcessPackLoopingServiceTask.GATEWAY_ELEMENT_ID); + } + + @Test + void testOccurredDuringJobFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob job = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, job.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(job).extractLatestIncident(); + + // then + assertThatThrownBy(() -> incidentAssert.occurredDuringJob(-1)) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith( + "Incident was not raised during job instance -1 but was raised in %d instead", + job.getKey()); + } + + @Test + void testIsResolvedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + assertThatThrownBy(incidentAssert::isResolved) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Incident is not resolved"); + } + + @Test + void testIsUnresolvedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MESSAGE); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + final long incidentKey = incidentAssert.getIncidentKey(); + client.newResolveIncidentCommand(incidentKey).send().join(); + + engine.waitForIdleState(); + + // then + assertThatThrownBy(incidentAssert::isUnresolved) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Incident is already resolved"); + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/JobAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/JobAssertTest.java new file mode 100644 index 000000000..57c57dac5 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/JobAssertTest.java @@ -0,0 +1,367 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.assertions; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.assertj.core.api.Assertions.entry; +import static org.assertj.core.data.Offset.offset; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ActivateJobsResponse; +import io.camunda.zeebe.client.api.response.ActivatedJob; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.assertions.IncidentAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackLoopingServiceTask; +import io.camunda.zeebe.protocol.record.value.ErrorType; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class JobAssertTest { + + public static final String WRONG_VALUE = "wrong value"; + public static final String ERROR_CODE = "error"; + public static final String ERROR_MSG = "error occurred"; + + // These tests are for testing assertions as well as examples for users + @Nested + class HappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + void testHasElementId() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual).hasElementId(ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + @Test + void testHasDeadline() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final long expectedDeadline = System.currentTimeMillis() + 100; + final ActivateJobsResponse jobActivationResponse = + client + .newActivateJobsCommand() + .jobType(ProcessPackLoopingServiceTask.JOB_TYPE) + .maxJobsToActivate(1) + .timeout(Duration.ofMillis(100)) + .send() + .join(); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual).hasDeadline(expectedDeadline, offset(50L)); + } + + @Test + void testHasBpmnProcessId() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual).hasBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + } + + @Test + void testHasRetries() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual).hasRetries(1); + } + + @Test + void testHasAnyIncidents() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MSG); + + // then + + BpmnAssert.assertThat(actual).hasAnyIncidents(); + } + + @Test + void testHasNoIncidents() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual).hasNoIncidents(); + } + + @Test + void testExtractLatestIncident() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MSG); + + final IncidentAssert incidentAssert = BpmnAssert.assertThat(actual).extractLatestIncident(); + + // then + Assertions.assertThat(incidentAssert).isNotNull(); + incidentAssert + .isUnresolved() + .hasErrorType(ErrorType.UNHANDLED_ERROR_EVENT) + .occurredDuringJob(actual); + } + + @Test + void testExtractingVariables() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual) + .extractingVariables() + .containsOnly( + entry(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1), entry("loopAmount", 0)); + } + + @Test + void testExtractingHeaders() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + BpmnAssert.assertThat(actual).extractingHeaders().isEmpty(); + } + } + + // These tests are just for assertion testing purposes. These should not be used as examples. + @Nested + class UnhappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + void testHasElementIdFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).hasElementId(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Job is not associated with expected element id '%s' but is instead associated with '%s'.", + WRONG_VALUE, ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + @Test + void testHasDeadlineFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final long expectedDeadline = System.currentTimeMillis() + 100; + final ActivateJobsResponse jobActivationResponse = + client + .newActivateJobsCommand() + .jobType(ProcessPackLoopingServiceTask.JOB_TYPE) + .maxJobsToActivate(1) + .timeout(Duration.ofMillis(100)) + .send() + .join(); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).hasDeadline(-1, offset(20L))) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll("Deadline", "-1", "20"); + } + + @Test + void testHasBpmnProcessIdFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).hasBpmnProcessId(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Job is not associated with BPMN process id '%s' but is instead associated with '%s'.", + WRONG_VALUE, ProcessPackLoopingServiceTask.PROCESS_ID); + } + + @Test + void testHasRetriesFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).hasRetries(12345)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Job does not have %d retries, as expected, but instead has %d retries.", 12345, 1); + } + + @Test + void testHasAnyIncidentsFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).hasAnyIncidents()) + .isInstanceOf(AssertionError.class) + .hasMessage("No incidents were raised for this job"); + } + + @Test + void testHasNoIncidentsFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + Utilities.throwErrorCommand(engine, client, actual.getKey(), ERROR_CODE, ERROR_MSG); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).hasNoIncidents()) + .isInstanceOf(AssertionError.class) + .hasMessage("Incidents were raised for this job"); + } + + @Test + void testExtractLatestIncidentFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // when + final ActivateJobsResponse jobActivationResponse = + Utilities.activateSingleJob(client, ProcessPackLoopingServiceTask.JOB_TYPE); + + // then + final ActivatedJob actual = jobActivationResponse.getJobs().get(0); + assertThatThrownBy(() -> BpmnAssert.assertThat(actual).extractLatestIncident()) + .isInstanceOf(AssertionError.class) + .hasMessage("No incidents were raised for this job"); + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/MessageAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/MessageAssertTest.java new file mode 100644 index 000000000..8ea7ea321 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/MessageAssertTest.java @@ -0,0 +1,320 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.assertions; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.zeebe.client.api.response.PublishMessageResponse; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackMessageEvent; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackMessageStartEvent; +import java.time.Duration; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class MessageAssertTest { + + public static final String CORRELATION_KEY = "correlationkey"; + public static final String WRONG_CORRELATION_KEY = "wrongcorrelationkey"; + public static final String WRONG_MESSAGE_NAME = "wrongmessagename"; + + @Nested + class HappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + void testHasBeenCorrelated() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Map variables = + Collections.singletonMap( + ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, CORRELATION_KEY); + Utilities.startProcessInstance(engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).hasBeenCorrelated(); + } + + @Test + void testHasMessageStartEventBeenCorrelated() { + // given + Utilities.deployProcess(client, ProcessPackMessageStartEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, + client, + ProcessPackMessageStartEvent.MESSAGE_NAME, + ProcessPackMessageStartEvent.CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).hasCreatedProcessInstance(); + } + + @Test + void testHasNotBeenCorrelated() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).hasNotBeenCorrelated(); + } + + @Test + void testHasMessageStartEventNotBeenCorrelated() { + // given + Utilities.deployProcess(client, ProcessPackMessageStartEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, WRONG_MESSAGE_NAME, ProcessPackMessageStartEvent.CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).hasNotCreatedProcessInstance(); + } + + @Test + void testHasExpired() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Duration timeToLive = Duration.ofDays(1); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY, timeToLive); + Utilities.increaseTime(engine, timeToLive.plusMinutes(1)); + + // then + BpmnAssert.assertThat(response).hasExpired(); + } + + @Test + void testHasNotExpired() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).hasNotExpired(); + } + + @Test + void testExtractingProcessInstance() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Map variables = + Collections.singletonMap( + ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, CORRELATION_KEY); + Utilities.startProcessInstance(engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).extractingProcessInstance().isCompleted(); + } + + @Test + void testExtractingProcessInstance_messageStartEvent() { + // given + Utilities.deployProcess(client, ProcessPackMessageStartEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, + client, + ProcessPackMessageStartEvent.MESSAGE_NAME, + ProcessPackMessageStartEvent.CORRELATION_KEY); + + // then + BpmnAssert.assertThat(response).extractingProcessInstance().isCompleted(); + } + } + + @Nested + class UnhappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + void testHasBeenCorrelatedFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).hasBeenCorrelated()) + .isInstanceOf(AssertionError.class) + .hasMessage("Message with key %d was not correlated", response.getMessageKey()); + } + + @Test + void testHasMessageStartEventBeenCorrelatedFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageStartEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, WRONG_MESSAGE_NAME, ProcessPackMessageStartEvent.CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).hasCreatedProcessInstance()) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Message with key %d did not lead to the creation of a process instance", + response.getMessageKey()); + } + + @Test + void testHasNotBeenCorrelatedFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Map variables = + Collections.singletonMap( + ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, CORRELATION_KEY); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).hasNotBeenCorrelated()) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Message with key %d was correlated to process instance %s", + response.getMessageKey(), instanceEvent.getProcessInstanceKey()); + } + + @Test + void testHasMessageStartEventNotBeenCorrelatedFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageStartEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, + client, + ProcessPackMessageStartEvent.MESSAGE_NAME, + ProcessPackMessageStartEvent.CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).hasNotCreatedProcessInstance()) + .isInstanceOf(AssertionError.class) + .hasMessageContaining( + "Message with key %d was correlated to process instance", response.getMessageKey()); + } + + @Test + void testHasExpiredFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).hasExpired()) + .isInstanceOf(AssertionError.class) + .hasMessage("Message with key %d has not expired", response.getMessageKey()); + } + + @Test + void testHasNotExpiredFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Duration timeToLive = Duration.ofDays(1); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, CORRELATION_KEY, timeToLive); + Utilities.increaseTime(engine, timeToLive.plusMinutes(1)); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).hasNotExpired()) + .isInstanceOf(AssertionError.class) + .hasMessage("Message with key %d has expired", response.getMessageKey()); + } + + @Test + void testExtractingProcessInstanceFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Map variables = + Collections.singletonMap( + ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, CORRELATION_KEY); + Utilities.startProcessInstance(engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, ProcessPackMessageEvent.MESSAGE_NAME, WRONG_CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).extractingProcessInstance()) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected to find one correlated process instance for message key %d but found %d: %s", + response.getMessageKey(), 0, "[]"); + } + + @Test + void testExtractingProcessInstanceFailure_messageStartEvent() { + // given + Utilities.deployProcess(client, ProcessPackMessageStartEvent.RESOURCE_NAME); + + // when + final PublishMessageResponse response = + Utilities.sendMessage( + engine, client, WRONG_MESSAGE_NAME, ProcessPackMessageStartEvent.CORRELATION_KEY); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(response).extractingProcessInstance()) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected to find one correlated process instance for message key %d but found %d: %s", + response.getMessageKey(), 0, "[]"); + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/ProcessAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/ProcessAssertTest.java new file mode 100644 index 000000000..0e077810b --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/ProcessAssertTest.java @@ -0,0 +1,243 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.assertions; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.DeploymentEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.assertions.ProcessAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackLoopingServiceTask; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class ProcessAssertTest { + + public static final String WRONG_VALUE = "wrong value"; + + // These tests are for testing assertions as well as examples for users + @Nested + class HappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testHasBPMNProcessId() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + processAssert.hasBPMNProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + } + + @Test + public void testHasVersion() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + processAssert.hasVersion(1); + } + + @Test + public void testHasResourceName() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + processAssert.hasResourceName(ProcessPackLoopingServiceTask.RESOURCE_NAME); + } + + @Test + public void testHasAnyInstances() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + processAssert.hasAnyInstances(); + } + + @Test + public void testHasNoInstances() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + processAssert.hasNoInstances(); + } + + @Test + public void testHasInstances() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + processAssert.hasInstances(2); + } + } + + // These tests are just for assertion testing purposes. These should not be used as examples. + @Nested + class UnhappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testHasBPMNProcessIdFailure() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> processAssert.hasBPMNProcessId(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected BPMN process ID to be '%s' but was '%s' instead.", + WRONG_VALUE, ProcessPackLoopingServiceTask.PROCESS_ID); + } + + @Test + public void testHasVersionFailure() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> processAssert.hasVersion(12345)) + .isInstanceOf(AssertionError.class) + .hasMessage("Expected version to be 12345 but was 1 instead"); + } + + @Test + public void testHasResourceNameFailure() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> processAssert.hasResourceName(WRONG_VALUE)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected resource name to be '%s' but was '%s' instead.", + WRONG_VALUE, ProcessPackLoopingServiceTask.RESOURCE_NAME); + } + + @Test + public void testHasAnyInstancesFailure() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(processAssert::hasAnyInstances) + .isInstanceOf(AssertionError.class) + .hasMessage("The process has no instances"); + } + + @Test + public void testHasNoInstances() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(processAssert::hasNoInstances) + .isInstanceOf(AssertionError.class) + .hasMessage("The process does have instances"); + } + + @Test + public void testHasInstances() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + final ProcessAssert processAssert = + BpmnAssert.assertThat(deploymentEvent) + .extractingProcessByBpmnProcessId(ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> processAssert.hasInstances(2)) + .isInstanceOf(AssertionError.class) + .hasMessage("Expected number of instances to be 2 but was 3 instead"); + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/ProcessInstanceAssertTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/ProcessInstanceAssertTest.java new file mode 100644 index 000000000..7f58a4788 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/assertions/ProcessInstanceAssertTest.java @@ -0,0 +1,1135 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.assertions; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.assertions.IncidentAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackLoopingServiceTask; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackMessageEvent; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackMultipleTasks; +import io.camunda.zeebe.protocol.record.value.ErrorType; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class ProcessInstanceAssertTest { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private static final Map TYPED_TEST_VARIABLES = new HashMap<>(); + + static { + TYPED_TEST_VARIABLES.put("stringProperty", "stringValue"); + TYPED_TEST_VARIABLES.put("numberProperty", 123); + TYPED_TEST_VARIABLES.put("booleanProperty", true); + TYPED_TEST_VARIABLES.put("complexProperty", Arrays.asList("Element 1", "Element 2")); + } + + // These tests are for testing assertions as well as examples for users + @Nested + class HappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testProcessInstanceIsStarted() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent).isStarted(); + } + + @Test + public void testProcessInstanceIsActive() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent).isActive(); + } + + @Test + public void testProcessInstanceIsCompleted() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + BpmnAssert.assertThat(instanceEvent).isCompleted(); + } + + @Test + public void testProcessInstanceIsNotCompleted() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent).isNotCompleted(); + } + + @Test + public void testProcessInstanceIsTerminated() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + client.newCancelInstanceCommand(instanceEvent.getProcessInstanceKey()).send().join(); + Utilities.waitForIdleState(engine); + + // then + BpmnAssert.assertThat(instanceEvent).isTerminated(); + } + + @Test + public void testProcessInstanceIsNotTerminated() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent).isNotTerminated(); + } + + @Test + public void testProcessInstanceHasPassedElement() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + BpmnAssert.assertThat(instanceEvent) + .hasPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + @Test + public void testProcessInstanceHasNotPassedElement() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent) + .hasNotPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + @Test + public void testProcessInstanceHasPassedElementMultipleTimes() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final int totalLoops = 5; + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, totalLoops); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + for (int i = 0; i < 5; i++) { + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + // then + BpmnAssert.assertThat(instanceEvent) + .hasPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID, totalLoops); + } + + @Test + public void testProcessInstanceHasPassedElementsInOrder() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + BpmnAssert.assertThat(instanceEvent) + .hasPassedElementInOrder( + ProcessPackLoopingServiceTask.START_EVENT_ID, + ProcessPackLoopingServiceTask.ELEMENT_ID, + ProcessPackLoopingServiceTask.END_EVENT_ID); + } + + @Test + public void testProcessInstanceIsWaitingAt() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // then + BpmnAssert.assertThat(instanceEvent) + .isWaitingAtElement(ProcessPackMultipleTasks.ELEMENT_ID_1); + } + + @Test + public void testProcessIsWaitingAtMultipleElements() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // then + BpmnAssert.assertThat(instanceEvent) + .isWaitingAtElement( + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceIsNotWaitingAt() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // when + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + + // then + BpmnAssert.assertThat(instanceEvent) + .isNotWaitingAtElement(ProcessPackMultipleTasks.ELEMENT_ID_1); + } + + @Test + public void testProcessInstanceIsNotWaitingAtMultipleElements() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // when + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_2); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_3); + + // then + BpmnAssert.assertThat(instanceEvent) + .isNotWaitingAtElement( + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceIsNotWaitingAtNonExistingElement() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + final String nonExistingElementId = "non-existing-task"; + + // then + BpmnAssert.assertThat(instanceEvent).isNotWaitingAtElement(nonExistingElementId); + } + + @Test + public void testProcessInstanceIsWaitingExactlyAtElements() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // when + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + + // then + BpmnAssert.assertThat(instanceEvent) + .isWaitingExactlyAtElements( + ProcessPackMultipleTasks.ELEMENT_ID_2, ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceIsWaitingForMessage() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, "key"); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent) + .isWaitingForMessage(ProcessPackMessageEvent.MESSAGE_NAME); + } + + @Test + public void testProcessInstanceIsNotWaitingForMessage() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap( + ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, correlationKey); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + Utilities.sendMessage(engine, client, ProcessPackMessageEvent.MESSAGE_NAME, correlationKey); + + // then + BpmnAssert.assertThat(instanceEvent) + .isNotWaitingForMessage(ProcessPackMessageEvent.MESSAGE_NAME); + } + + @Test + public void testProcessInstanceHasVariable() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent).hasVariable(ProcessPackLoopingServiceTask.TOTAL_LOOPS); + } + + @Test + public void testProcessInstanceHasVariableWithValue() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, TYPED_TEST_VARIABLES); + + // then + final SoftAssertions softly = new SoftAssertions(); + + TYPED_TEST_VARIABLES.forEach( + (key, value) -> BpmnAssert.assertThat(instanceEvent).hasVariableWithValue(key, value)); + + softly.assertAll(); + } + + @Test + public void testHasCorrelatedMessageByName() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap("correlationKey", correlationKey); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + Utilities.sendMessage(engine, client, ProcessPackMessageEvent.MESSAGE_NAME, correlationKey); + + // then + BpmnAssert.assertThat(instanceEvent) + .hasCorrelatedMessageByName(ProcessPackMessageEvent.MESSAGE_NAME, 1); + } + + @Test + public void testHasCorrelatedMessageByCorrelationKey() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap( + ProcessPackMessageEvent.CORRELATION_KEY_VARIABLE, correlationKey); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + Utilities.sendMessage(engine, client, ProcessPackMessageEvent.MESSAGE_NAME, correlationKey); + + // then + BpmnAssert.assertThat(instanceEvent).hasCorrelatedMessageByCorrelationKey(correlationKey, 1); + } + + @Test + public void testHasAnyIncidents() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + final Map variables = + Collections.singletonMap( + ProcessPackLoopingServiceTask.TOTAL_LOOPS, "invalid value"); // will cause incident + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + /* will raise an incident in the gateway because ProcessPackLoopingServiceTask.TOTAL_LOOPS is a string, but needs to be an int */ + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + BpmnAssert.assertThat(instanceEvent).hasAnyIncidents(); + } + + @Test + public void testHasNoIncidents() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + BpmnAssert.assertThat(instanceEvent).hasNoIncidents(); + } + + @Test + public void testExtractLatestIncident() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + final Map variables = + Collections.singletonMap( + ProcessPackLoopingServiceTask.TOTAL_LOOPS, "invalid value"); // will cause incident + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + /* will raise an incident in the gateway because ProcessPackLoopingServiceTask.TOTAL_LOOPS is a string, but needs to be an int */ + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + final IncidentAssert incidentAssert = + BpmnAssert.assertThat(instanceEvent).extractLatestIncident(); + + // then + + Assertions.assertThat(incidentAssert).isNotNull(); + incidentAssert + .isUnresolved() + .hasErrorType(ErrorType.EXTRACT_VALUE_ERROR) + .wasRaisedInProcessInstance(instanceEvent); + } + } + + // These tests are just for assertion testing purposes. These should not be used as examples. + @Nested + class UnhappyPathTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testProcessInstanceIsStartedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent mockInstanceEvent = mock(ProcessInstanceEvent.class); + + // when + when(mockInstanceEvent.getProcessInstanceKey()).thenReturn(-1L); + + // then + org.junit.jupiter.api.Assertions.assertThrows( + AssertionError.class, + BpmnAssert.assertThat(mockInstanceEvent)::isStarted, + "Process with key -1 was not started"); + } + + @Test + public void testProcessInstanceIsNotStartedIfProcessInstanceKeyNoMatch() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + final ProcessInstanceEvent mockInstanceEvent = mock(ProcessInstanceEvent.class); + + // when + when(mockInstanceEvent.getProcessInstanceKey()).thenReturn(-1L); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(mockInstanceEvent).isStarted()) + .isInstanceOf(AssertionError.class) + .hasMessage("Process with key -1 was not started"); + } + + @Test + public void testProcessInstanceIsActiveFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).isActive()) + .isInstanceOf(AssertionError.class) + .hasMessage("Process with key %s is not active", instanceEvent.getProcessInstanceKey()); + } + + @Test + public void testProcessInstanceIsCompletedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).isCompleted()) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Process with key %s was not completed", instanceEvent.getProcessInstanceKey()); + } + + @Test + public void testProcessInstanceIsNotCompletedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).isNotCompleted()) + .isInstanceOf(AssertionError.class) + .hasMessage("Process with key %s was completed", instanceEvent.getProcessInstanceKey()); + } + + @Test + public void testProcessInstanceIsTerminatedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).isTerminated()) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Process with key %s was not terminated", instanceEvent.getProcessInstanceKey()); + } + + @Test + public void testProcessInstanceIsNotTerminatedFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // when + client.newCancelInstanceCommand(instanceEvent.getProcessInstanceKey()).send().join(); + Utilities.waitForIdleState(engine); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).isNotTerminated()) + .isInstanceOf(AssertionError.class) + .hasMessage("Process with key %s was terminated", instanceEvent.getProcessInstanceKey()); + } + + @Test + public void testProcessInstanceHasPassedElementFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .hasPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected element with id %s to be passed 1 times, but was 0", + ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + @Test + public void testProcessInstanceHasNotPassedElementFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, + client, + ProcessPackLoopingServiceTask.PROCESS_ID, + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1)); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .hasNotPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected element with id %s to be passed 0 times, but was 1", + ProcessPackLoopingServiceTask.ELEMENT_ID); + } + + @Test + public void testProcessInstanceHasPassedElementsInOrderFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 1); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // when + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .hasPassedElementInOrder( + ProcessPackLoopingServiceTask.END_EVENT_ID, + ProcessPackLoopingServiceTask.ELEMENT_ID, + ProcessPackLoopingServiceTask.START_EVENT_ID)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "[Ordered elements] " + + LINE_SEPARATOR + + "expected: [\"endevent\", \"servicetask\", \"startevent\"]" + + LINE_SEPARATOR + + " but was: [\"startevent\", \"servicetask\", \"endevent\"]"); + } + + @Test + public void testProcessInstanceIsWaitingAtFailure() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // when + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isWaitingAtElement(ProcessPackMultipleTasks.ELEMENT_ID_1)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll("to contain", ProcessPackMultipleTasks.ELEMENT_ID_1); + } + + @Test + public void testProcessInstanceIsWaitingAtMultipleElementsFailure() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // when + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_2); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_3); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isWaitingAtElement( + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll( + "to contain:", + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceWaitingAtNonExistingElementFailure() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + final String nonExistingTaskId = "non-existing-task"; + + // then + assertThatThrownBy( + () -> BpmnAssert.assertThat(instanceEvent).isWaitingAtElement(nonExistingTaskId)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll("to contain", nonExistingTaskId); + } + + @Test + public void testProcessInstanceIsNotWaitingAtFailure() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isNotWaitingAtElement(ProcessPackMultipleTasks.ELEMENT_ID_1)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll("not to contain", ProcessPackMultipleTasks.ELEMENT_ID_1); + } + + @Test + public void testProcessInstanceIsNotWaitingAtMulitpleElementsFailure() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isNotWaitingAtElement( + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll( + "not to contain", + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceIsWaitingExactlyAtElementsFailure_tooManyElements() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isWaitingExactlyAtElements(ProcessPackMultipleTasks.ELEMENT_ID_1)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll( + String.format( + "Process with key %s is waiting at element(s) with id(s)", + instanceEvent.getProcessInstanceKey()), + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3) + .hasMessageNotContaining(ProcessPackMultipleTasks.ELEMENT_ID_1); + } + + @Test + public void testProcessInstanceIsWaitingExactlyAtElementsFailure_tooLittleElements() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_2); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isWaitingExactlyAtElements( + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + ProcessPackMultipleTasks.ELEMENT_ID_3)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll( + String.format( + "Process with key %s is not waiting at element(s) with id(s)", + instanceEvent.getProcessInstanceKey()), + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2) + .hasMessageNotContaining(ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceIsWaitingExactlyAtElementsFailure_combination() { + // given + Utilities.deployProcess(client, ProcessPackMultipleTasks.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackMultipleTasks.PROCESS_ID); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_1); + Utilities.completeTask(engine, client, ProcessPackMultipleTasks.ELEMENT_ID_2); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isWaitingExactlyAtElements( + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll( + String.format( + "Process with key %s is not waiting at element(s) with id(s)", + instanceEvent.getProcessInstanceKey()), + ProcessPackMultipleTasks.ELEMENT_ID_1, + ProcessPackMultipleTasks.ELEMENT_ID_2, + String.format( + "Process with key %s is waiting at element(s) with id(s)", + instanceEvent.getProcessInstanceKey()), + ProcessPackMultipleTasks.ELEMENT_ID_3); + } + + @Test + public void testProcessInstanceIsWaitingForMessageFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap("correlationKey", correlationKey); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // when + Utilities.sendMessage(engine, client, ProcessPackMessageEvent.MESSAGE_NAME, correlationKey); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isWaitingForMessage(ProcessPackMessageEvent.MESSAGE_NAME)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll("to contain:", ProcessPackMessageEvent.MESSAGE_NAME); + } + + @Test + public void testProcessInstanceIsNotWaitingForMessageFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap("correlationKey", correlationKey); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .isNotWaitingForMessage(ProcessPackMessageEvent.MESSAGE_NAME)) + .isInstanceOf(AssertionError.class) + .hasMessageContainingAll("not to contain", ProcessPackMessageEvent.MESSAGE_NAME); + } + + @Test + public void testProcessInstanceHasVariableFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final String expectedVariable = "variable"; + final String actualVariable = "loopAmount"; + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).hasVariable(expectedVariable)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Process with key %s does not contain variable with name `%s`. Available variables are: [%s]", + instanceEvent.getProcessInstanceKey(), expectedVariable, actualVariable); + } + + @Test + public void testProcessInstanceHasVariableWithValueFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final String variable = "variable"; + final String expectedValue = "expectedValue"; + final String actualValue = "actualValue"; + final Map variables = Collections.singletonMap(variable, actualValue); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .hasVariableWithValue(variable, expectedValue)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "The variable '%s' does not have the expected value. The value passed in" + + " ('%s') is internally mapped to a JSON String that yields '\"%s\"'. However, the " + + "actual value (as JSON String) is '\"%s\"'.", + variable, expectedValue, expectedValue, actualValue); + } + + @Test + public void testHasCorrelatedMessageByNameFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap("correlationKey", correlationKey); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .hasCorrelatedMessageByName(ProcessPackMessageEvent.MESSAGE_NAME, 1)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected message with name '%s' to be correlated %d times, but was %d times", + ProcessPackMessageEvent.MESSAGE_NAME, 1, 0); + } + + @Test + public void testHasCorrelatedMessageByCorrelationKeyFailure() { + // given + Utilities.deployProcess(client, ProcessPackMessageEvent.RESOURCE_NAME); + final String correlationKey = "key"; + final Map variables = + Collections.singletonMap("correlationKey", correlationKey); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackMessageEvent.PROCESS_ID, variables); + + // then + assertThatThrownBy( + () -> + BpmnAssert.assertThat(instanceEvent) + .hasCorrelatedMessageByCorrelationKey(correlationKey, 1)) + .isInstanceOf(AssertionError.class) + .hasMessage( + "Expected message with correlation key '%s' to be correlated %d " + + "times, but was %d times", + correlationKey, 1, 0); + } + + @Test + public void testHasAnyIncidentsFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).hasAnyIncidents()) + .isInstanceOf(AssertionError.class) + .hasMessage("No incidents were raised for this process instance"); + } + + @Test + public void testHasNoIncidentsFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap( + ProcessPackLoopingServiceTask.TOTAL_LOOPS, "invalid value"); // will cause incident + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + /* will raise an incident in the gateway because ProcessPackLoopingServiceTask.TOTAL_LOOPS is a string, but needs to be an int */ + Utilities.completeTask(engine, client, ProcessPackLoopingServiceTask.ELEMENT_ID); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).hasNoIncidents()) + .isInstanceOf(AssertionError.class) + .hasMessage("Incidents were raised for this process instance"); + } + + @Test + public void testExtractLatestIncidentFailure() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackLoopingServiceTask.PROCESS_ID); + + // then + assertThatThrownBy(() -> BpmnAssert.assertThat(instanceEvent).extractLatestIncident()) + .isInstanceOf(AssertionError.class) + .hasMessage("No incidents were raised for this process instance"); + } + } + + // These tests validate bug fixes for bugs that have occurred in the past + @Nested + class RegressionTests { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test // regression test for #78 + public void testShouldCaptureLatestValueOfVariable() { + // given + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables1 = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, "1"); + + final Map variables2 = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, "2"); + + final Map variables3 = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, "3"); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables1); + + client + .newSetVariablesCommand(instanceEvent.getProcessInstanceKey()) + .variables(variables2) + .send() + .join(); + client + .newSetVariablesCommand(instanceEvent.getProcessInstanceKey()) + .variables(variables3) + .send() + .join(); + + Utilities.waitForIdleState(engine); + + // then + BpmnAssert.assertThat(instanceEvent) + .hasVariableWithValue(ProcessPackLoopingServiceTask.TOTAL_LOOPS, "3"); + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/inspections/ProcessEventInspectionsTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/inspections/ProcessEventInspectionsTest.java new file mode 100644 index 000000000..d73261275 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/inspections/ProcessEventInspectionsTest.java @@ -0,0 +1,94 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.inspections; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.DeploymentEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.inspections.InspectionUtility; +import io.camunda.zeebe.process.test.inspections.model.InspectedProcessInstance; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackTimerStartEvent; +import java.time.Duration; +import java.util.Optional; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +class ProcessEventInspectionsTest { + + private static final String WRONG_TIMER_ID = "wrongtimer"; + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testFindFirstProcessInstance() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackTimerStartEvent.RESOURCE_NAME); + + // when + Utilities.increaseTime(engine, Duration.ofDays(1)); + final Optional firstProcessInstance = + InspectionUtility.findProcessEvents() + .triggeredByTimer(ProcessPackTimerStartEvent.TIMER_ID) + .withProcessDefinitionKey( + deploymentEvent.getProcesses().get(0).getProcessDefinitionKey()) + .findFirstProcessInstance(); + + // then + Assertions.assertThat(firstProcessInstance).isNotEmpty(); + BpmnAssert.assertThat(firstProcessInstance.get()).isCompleted(); + } + + @Test + public void testFindLastProcessInstance() { + // given + final DeploymentEvent deploymentEvent = + Utilities.deployProcess(client, ProcessPackTimerStartEvent.RESOURCE_NAME); + + // when + Utilities.increaseTime(engine, Duration.ofDays(1)); + final Optional lastProcessInstance = + InspectionUtility.findProcessEvents() + .triggeredByTimer(ProcessPackTimerStartEvent.TIMER_ID) + .withProcessDefinitionKey( + deploymentEvent.getProcesses().get(0).getProcessDefinitionKey()) + .findLastProcessInstance(); + + // then + Assertions.assertThat(lastProcessInstance).isNotEmpty(); + BpmnAssert.assertThat(lastProcessInstance.get()).isCompleted(); + } + + @Test + public void testFindFirstProcessInstance_wrongTimer() { + // given + Utilities.deployProcess(client, ProcessPackTimerStartEvent.RESOURCE_NAME); + + // when + Utilities.increaseTime(engine, Duration.ofDays(1)); + final Optional processInstance = + InspectionUtility.findProcessEvents() + .triggeredByTimer(WRONG_TIMER_ID) + .findFirstProcessInstance(); + + // then + Assertions.assertThat(processInstance).isEmpty(); + } + + @Test + public void testFindProcessInstance_highIndex() { + // given + Utilities.deployProcess(client, ProcessPackTimerStartEvent.RESOURCE_NAME); + + // when + Utilities.increaseTime(engine, Duration.ofDays(1)); + final Optional processInstance = + InspectionUtility.findProcessEvents().findProcessInstance(10); + + // then + Assertions.assertThat(processInstance).isEmpty(); + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/inspections/ProcessInstanceInspectionsTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/inspections/ProcessInstanceInspectionsTest.java new file mode 100644 index 000000000..42fecf605 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/inspections/ProcessInstanceInspectionsTest.java @@ -0,0 +1,67 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.inspections; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.inspections.InspectionUtility; +import io.camunda.zeebe.process.test.inspections.model.InspectedProcessInstance; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackCallActivity; +import java.util.Optional; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +public class ProcessInstanceInspectionsTest { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testStartedByProcessInstanceWithProcessId() { + // given + Utilities.deployProcesses( + client, + ProcessPackCallActivity.RESOURCE_NAME, + ProcessPackCallActivity.CALLED_RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackCallActivity.PROCESS_ID); + + // when + final Optional firstProcessInstance = + InspectionUtility.findProcessInstances() + .withParentProcessInstanceKey(instanceEvent.getProcessInstanceKey()) + .withBpmnProcessId(ProcessPackCallActivity.CALLED_PROCESS_ID) + .findFirstProcessInstance(); + + // then + Assertions.assertThat(firstProcessInstance).isNotEmpty(); + BpmnAssert.assertThat(firstProcessInstance.get()).isCompleted(); + BpmnAssert.assertThat(instanceEvent) + .hasPassedElement(ProcessPackCallActivity.CALL_ACTIVITY_ID) + .isCompleted(); + } + + @Test + public void testStartedByProcessInstanceWithProcessId_wrongId() { + // given + Utilities.deployProcesses( + client, + ProcessPackCallActivity.RESOURCE_NAME, + ProcessPackCallActivity.CALLED_RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance(engine, client, ProcessPackCallActivity.PROCESS_ID); + + // when + final Optional firstProcessInstance = + InspectionUtility.findProcessInstances() + .withParentProcessInstanceKey(instanceEvent.getProcessInstanceKey()) + .withBpmnProcessId("wrongId") + .findFirstProcessInstance(); + + // then + Assertions.assertThat(firstProcessInstance).isEmpty(); + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/multithread/MultiThreadTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/multithread/MultiThreadTest.java new file mode 100644 index 000000000..5b46b1e62 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/multithread/MultiThreadTest.java @@ -0,0 +1,81 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.multithread; + +import static io.camunda.zeebe.process.test.assertions.BpmnAssert.assertThat; +import static io.camunda.zeebe.process.test.qa.util.Utilities.deployProcess; +import static io.camunda.zeebe.process.test.qa.util.Utilities.startProcessInstance; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.filters.RecordStream; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackStartEndEvent; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +public class MultiThreadTest { + + private InMemoryEngine engine; + private ZeebeClient client; + private RecordStream recordStream; + private ExecutorService executorService; + + @BeforeEach + public void beforeEach() { + executorService = Executors.newFixedThreadPool(5); + } + + @AfterEach + public void afterEach() { + executorService.shutdown(); + } + + @Test + public void testMultiThreadingThrowsNoExceptions() throws InterruptedException { + final List> futures = + executorService.invokeAll( + Arrays.asList( + new ProcessRunner(), + new ProcessRunner(), + new ProcessRunner(), + new ProcessRunner(), + new ProcessRunner())); + + for (final Future future : futures) { + try { + Assertions.assertThat(future.get()).isTrue(); + } catch (ExecutionException ex) { + Assertions.fail("Future completed exceptionally: %s", ExceptionUtils.getStackTrace(ex)); + } + } + } + + private class ProcessRunner implements Callable { + + @Override + public Boolean call() { + BpmnAssert.initRecordStream(recordStream); + + deployProcess(client, ProcessPackStartEndEvent.RESOURCE_NAME); + final ProcessInstanceEvent instanceEvent = + startProcessInstance(engine, client, ProcessPackStartEndEvent.PROCESS_ID); + engine.waitForIdleState(); + + assertThat(instanceEvent).isCompleted(); + BpmnAssert.resetRecordStream(); + return true; + } + } +} diff --git a/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/multithread/WorkerTest.java b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/multithread/WorkerTest.java new file mode 100644 index 000000000..8a90684a6 --- /dev/null +++ b/qa/src/test/java/io/camunda/zeebe/process/test/qa/testcontainer/multithread/WorkerTest.java @@ -0,0 +1,50 @@ +package io.camunda.zeebe.process.test.qa.testcontainer.multithread; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.client.api.response.ProcessInstanceEvent; +import io.camunda.zeebe.process.test.api.InMemoryEngine; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.extension.testcontainer.ZeebeProcessTest; +import io.camunda.zeebe.process.test.qa.util.Utilities; +import io.camunda.zeebe.process.test.qa.util.Utilities.ProcessPackLoopingServiceTask; +import java.util.Collections; +import java.util.Map; +import org.junit.jupiter.api.Test; + +@ZeebeProcessTest +public class WorkerTest { + + private ZeebeClient client; + private InMemoryEngine engine; + + @Test + public void testJobsCanBeProcessedAsynchronouslyByWorker() throws InterruptedException { + // given + client + .newWorker() + .jobType(ProcessPackLoopingServiceTask.JOB_TYPE) + .handler( + (client, job) -> { + client.newCompleteCommand(job.getKey()).send(); + }) + .open(); + + Utilities.deployProcess(client, ProcessPackLoopingServiceTask.RESOURCE_NAME); + final Map variables = + Collections.singletonMap(ProcessPackLoopingServiceTask.TOTAL_LOOPS, 3); + + // when + final ProcessInstanceEvent instanceEvent = + Utilities.startProcessInstance( + engine, client, ProcessPackLoopingServiceTask.PROCESS_ID, variables); + + // then + BpmnAssert.assertThat(instanceEvent).isStarted(); + // TODO: Idle state monitor does not work in this case. + // Might be fixed when switching to the zeebe built-in idle state monitor + Thread.sleep(1000); + BpmnAssert.assertThat(instanceEvent) + .hasPassedElement(ProcessPackLoopingServiceTask.ELEMENT_ID, 3) + .isCompleted(); + } +}