diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/Generator.java b/impl-8/src/main/java/org/camunda/community/bpmndt/Generator.java index e4fb452..1bfd7f1 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/Generator.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/Generator.java @@ -11,6 +11,7 @@ import org.camunda.community.bpmndt.api.CustomMultiInstanceHandler; import org.camunda.community.bpmndt.api.JobHandler; import org.camunda.community.bpmndt.api.MessageEventHandler; +import org.camunda.community.bpmndt.api.OutboundConnectorHandler; import org.camunda.community.bpmndt.api.SignalEventHandler; import org.camunda.community.bpmndt.api.TestCaseExecutor; import org.camunda.community.bpmndt.api.TestCaseInstance; @@ -92,6 +93,7 @@ public void generate(GeneratorContext ctx) { apiClasses.add(CustomMultiInstanceHandler.class); apiClasses.add(JobHandler.class); apiClasses.add(MessageEventHandler.class); + apiClasses.add(OutboundConnectorHandler.class); apiClasses.add(SignalEventHandler.class); apiClasses.add(TestCaseExecutor.class); apiClasses.add(TestCaseInstance.class); diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/CallActivityHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/CallActivityHandler.java index 5796106..15e6720 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/CallActivityHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/CallActivityHandler.java @@ -9,6 +9,9 @@ import io.camunda.zeebe.process.test.assertions.BpmnAssert; import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +/** + * Fluent API to handle call activities. The called process is simulated - see {@link TestCaseExecutor#simulateProcess(String)} + */ public class CallActivityHandler { // see src/main/resources/simulate-sub-process.bpmn diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java index 182dbf3..b0cd4ca 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java @@ -9,7 +9,7 @@ import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; /** - * Fluent API to handle multi instances and multi instance scopes, using custom code. + * Fluent API to handle multi instances and multi instance scopes, using custom code - see {@link #execute(BiConsumer)}. */ public class CustomMultiInstanceHandler { diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/JobHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/JobHandler.java index 3476b72..3e6bba6 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/JobHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/JobHandler.java @@ -12,7 +12,7 @@ import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; /** - * Fluent API to handle jobs that are handled via {@code JobHandler} by worker. + * Fluent API to handle jobs in form of service, script, send or business rule tasks as well as intermediate message throw or message end events. */ public class JobHandler { diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/MessageEventHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/MessageEventHandler.java index ff14e0f..d5f6fdc 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/MessageEventHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/MessageEventHandler.java @@ -10,6 +10,9 @@ import io.camunda.zeebe.process.test.assertions.BpmnAssert; import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +/** + * Fluent API to handle message catch events. + */ public class MessageEventHandler { private final MessageEventElement element; diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/OutboundConnectorHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/OutboundConnectorHandler.java index a6b3faa..e12def7 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/OutboundConnectorHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/OutboundConnectorHandler.java @@ -1,8 +1,308 @@ package org.camunda.community.bpmndt.api; +import java.util.HashMap; +import java.util.Map; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import org.camunda.community.bpmndt.api.TestCaseInstanceElement.OutboundConnectorElement; + +import io.camunda.zeebe.client.ZeebeClient; +import io.camunda.zeebe.process.test.assertions.BpmnAssert; +import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; + +/** + * Fluent API to handle outbound connectors. + */ public class OutboundConnectorHandler { - public OutboundConnectorHandler() { - // TODO + private final OutboundConnectorElement element; + + private final Map variableMap = new HashMap<>(); + + private Consumer verifier; + private BiConsumer action; + private String errorCode; + private String errorMessage; + private Object variables; + + private Consumer> inputMappingConsumer; + private Consumer> outputMappingConsumer; + private Consumer taskDefinitionTypeConsumer; + private Consumer> taskHeadersConsumer; + + private Integer expectedRetries; + private String expectedTaskDefinitionType; + + private Consumer retriesConsumer; + + public OutboundConnectorHandler(String elementId) { + if (elementId == null) { + throw new IllegalArgumentException("element ID is null"); + } + + element = new OutboundConnectorElement(); + element.id = elementId; + + complete(); + } + + public OutboundConnectorHandler(OutboundConnectorElement element) { + if (element == null) { + throw new IllegalArgumentException("element is null"); + } + if (element.id == null) { + throw new IllegalArgumentException("element ID is null"); + } + + this.element = element; + + complete(); + } + + void apply(TestCaseInstance instance, long processInstanceKey) { + if (verifier != null) { + verifier.accept(new ProcessInstanceAssert(processInstanceKey, BpmnAssert.getRecordStream())); + } + + if (expectedTaskDefinitionType != null && !expectedTaskDefinitionType.equals(element.taskDefinitionType)) { + var message = "expected outbound connector %s to have task definition type '%s', but was '%s'"; + throw new AssertionError(String.format(message, element.id, expectedTaskDefinitionType, element.taskDefinitionType)); + } + if (taskDefinitionTypeConsumer != null) { + taskDefinitionTypeConsumer.accept(element.taskDefinitionType); + } + + if (taskHeadersConsumer != null) { + taskHeadersConsumer.accept(element.taskHeaders); + } + + if (inputMappingConsumer != null) { + inputMappingConsumer.accept(element.inputs); + } + if (outputMappingConsumer != null) { + outputMappingConsumer.accept(element.outputs); + } + + var job = instance.getJob(processInstanceKey, element.id); + + if (expectedRetries != null && !expectedRetries.equals(job.retries)) { + var message = "expected job %s to have a retry count of %d, but was %d"; + throw new AssertionError(String.format(message, element.id, expectedRetries, job.retries)); + } + if (retriesConsumer != null) { + retriesConsumer.accept(job.retries); + } + + if (action != null) { + action.accept(instance.getClient(), job.key); + } + } + + /** + * Executes an action that completes the underlying job, when the process instance is waiting at the corresponding element, using specified variables. + * + * @see #withVariable(String, Object) + * @see #withVariables(Object) + * @see #withVariableMap(Map) + */ + public void complete() { + action = this::complete; + } + + /** + * Customizes the handler, using the given {@link Consumer} function. This method can be used to apply a common customization needed for different test + * cases. + * + *
+   * tc.handleOutboundConnector().customize(this::prepare);
+   * 
+ * + * @param customizer A function that accepts a {@link OutboundConnectorHandler}. + * @return The handler. + */ + public OutboundConnectorHandler customize(Consumer customizer) { + if (customizer != null) { + customizer.accept(this); + } + return this; + } + + /** + * Executes a custom action that handles the underlying job, when the process instance is waiting at the corresponding element. + * + * @param action A specific action that accepts a {@link ZeebeClient} and the related job key. + * @see ZeebeClient#newCompleteCommand(long) + */ + public void execute(BiConsumer action) { + if (action == null) { + throw new IllegalArgumentException("action is null"); + } + this.action = action; + } + + /** + * Throws an BPMN error using the given error code and message as well as the specified variables. + * + * @see #withVariable(String, Object) + * @see #withVariables(Object) + * @see #withVariableMap(Map) + */ + public void throwBpmnError(String errorCode, String errorMessage) { + this.errorCode = errorCode; + this.errorMessage = errorMessage; + this.action = this::throwBpmnError; + } + + /** + * Verifies the outbound connector's waiting state. This method can be used to assert the variables, created by the connector's input mapping. + * + * @param verifier Verifier that accepts an {@link ProcessInstanceAssert} instance. + * @return The handler. + */ + public OutboundConnectorHandler verify(Consumer verifier) { + this.verifier = verifier; + return this; + } + + /** + * Verifies the modeled inputs of the connector's IO mapping, using a consumer function. + * + * @param inputMappingConsumer A consumer asserting the input mapping. + * @return The handler. + */ + public OutboundConnectorHandler verifyInputMapping(Consumer> inputMappingConsumer) { + this.inputMappingConsumer = inputMappingConsumer; + return this; + } + + /** + * Verifies the modeled outputs of the connector's IO mapping, using a consumer function. + * + * @param outputMappingConsumer A consumer asserting the output mapping. + * @return The handler. + */ + public OutboundConnectorHandler verifyOutputMapping(Consumer> outputMappingConsumer) { + this.outputMappingConsumer = outputMappingConsumer; + return this; + } + + /** + * Verifies that the underlying job has a specific number of retries. + * + * @param expectedRetries The expected retry count. + * @return The handler. + */ + public OutboundConnectorHandler verifyRetries(Integer expectedRetries) { + this.expectedRetries = expectedRetries; + return this; + } + + /** + * Verifies that the underlying job has a specific number of retries, using a consumer. + * + * @param retriesConsumer A consumer asserting the retry count. + * @return The handler. + */ + public OutboundConnectorHandler verifyRetries(Consumer retriesConsumer) { + this.retriesConsumer = retriesConsumer; + return this; + } + + /** + * Verifies that the outbound connector has the given task definition type. + * + * @param expectedTaskDefinitionType The expected task definition type. + * @return The handler. + */ + public OutboundConnectorHandler verifyTaskDefinitionType(String expectedTaskDefinitionType) { + this.expectedTaskDefinitionType = expectedTaskDefinitionType; + return this; + } + + /** + * Verifies that the outbound connector has a specific task definition type, using a consumer. + * + * @param taskDefinitionTypeConsumer A consumer asserting the task definition type. + * @return The handler. + */ + public OutboundConnectorHandler verifyTaskDefinitionType(Consumer taskDefinitionTypeConsumer) { + this.taskDefinitionTypeConsumer = taskDefinitionTypeConsumer; + return this; + } + + /** + * Verifies the modeled task headers of the connector, using a consumer function. + * + * @param taskHeadersConsumer A consumer asserting the task headers. + * @return The handler. + */ + public OutboundConnectorHandler verifyTaskHeaders(Consumer> taskHeadersConsumer) { + this.taskHeadersConsumer = taskHeadersConsumer; + return this; + } + + /** + * Sets a variable that is used to complete the underlying job. + * + * @param name The name of the variable. + * @param value The variable's value. + * @return The handler. + * @see #complete() + */ + public OutboundConnectorHandler withVariable(String name, Object value) { + if (variables != null) { + throw new IllegalStateException("either use an object (POJO) as variables or a variable map"); + } + variableMap.put(name, value); + return this; + } + + /** + * Sets an object as variables that is used to complete the underlying job. + * + * @param variables The variables as POJO. + * @return The handler. + * @see #complete() + */ + public OutboundConnectorHandler withVariables(Object variables) { + if (!variableMap.isEmpty()) { + throw new IllegalStateException("either use an object (POJO) as variables or a variable map"); + } + this.variables = variables; + return this; + } + + /** + * Sets variables that are used to complete the underlying job. + * + * @param variableMap A map of variables. + * @return The handler. + * @see #complete() + */ + public OutboundConnectorHandler withVariableMap(Map variableMap) { + if (variables != null) { + throw new IllegalStateException("either use an object (POJO) as variables or a variable map"); + } + this.variableMap.putAll(variableMap); + return this; + } + + void complete(ZeebeClient client, long jobKey) { + if (variables != null) { + client.newCompleteCommand(jobKey).variables(variables).send().join(); + } else { + client.newCompleteCommand(jobKey).variables(variableMap).send().join(); + } + } + + void throwBpmnError(ZeebeClient client, long jobKey) { + var throwErrorCommandStep2 = client.newThrowErrorCommand(jobKey).errorCode(errorCode).errorMessage(errorMessage); + + if (variables != null) { + throwErrorCommandStep2.variables(variables).send().join(); + } else { + throwErrorCommandStep2.variables(variableMap).send().join(); + } } } diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/SignalEventHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/SignalEventHandler.java index 9bf7504..bd21d89 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/SignalEventHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/SignalEventHandler.java @@ -9,6 +9,9 @@ import io.camunda.zeebe.process.test.assertions.BpmnAssert; import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +/** + * Fluent API to handle signal catch events. + */ public class SignalEventHandler { private final SignalEventElement element; diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstance.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstance.java index 7e96eb3..9fad1c4 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstance.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstance.java @@ -90,6 +90,10 @@ public void apply(long processInstanceKey, MessageEventHandler handler) { handler.apply(this, processInstanceKey); } + public void apply(long processInstanceKey, OutboundConnectorHandler handler) { + handler.apply(this, processInstanceKey); + } + public void apply(long processInstanceKey, SignalEventHandler handler) { handler.apply(this, processInstanceKey); } @@ -322,8 +326,8 @@ TimerMemo getTimer(long processInstanceKey, String elementId) { } /** - * Consumes all records that has not been consumed already. Additionally select as well as select and test tasks that are waiting to be executed are handled - * using the memorization. + * Consumes all records that has not been consumed already. Additionally select and/or select and test tasks that are waiting to be executed are handled using + * the memorization. */ private void consumeRecordStream() { var recordStream = RecordStream.of(engine.getRecordStreamSource()); @@ -380,6 +384,14 @@ private void consumeRecordStream() { } } + /** + * Selects information from the memorization, using the given function. The function is applied every 100ms until the result is not {@code null} or the task + * timeout expired. + * + * @param selector Selector function. + * @param The result type. + * @return The result. + */ private T select(Function selector) { SelectTask task = new SelectTask<>(selector); @@ -399,6 +411,13 @@ private T select(Function selector) { return task.result; } + /** + * Selects and tests information from the memorization, using the given predicate. The predicate is tested every 100ms until it is {@code true} or the task + * timeout expired. + * + * @param predicate The predicate to test. + * @return The test result. + */ private boolean selectAndTest(Predicate predicate) { selectAndTestTask = new SelectAndTestTask(predicate); try { @@ -409,7 +428,7 @@ private boolean selectAndTest(Predicate predicate) { Thread.currentThread().interrupt(); } - return selectAndTestTask == null; + return selectAndTestTask == null; // if true, the reference is set to null } private String withDetails(String message, long processInstanceKey) { diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceElement.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceElement.java index 246518c..d548aa6 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceElement.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceElement.java @@ -1,5 +1,7 @@ package org.camunda.community.bpmndt.api; +import java.util.Map; + /** * BPMN element, containing the information from a parsed BPMN model (design time) at runtime, used by specific handler. */ @@ -34,6 +36,14 @@ public static class MultiInstanceElement extends TestCaseInstanceElement { public boolean sequential; } + public static class OutboundConnectorElement extends TestCaseInstanceElement { + + public Map inputs; + public Map outputs; + public String taskDefinitionType; + public Map taskHeaders; + } + public static class SignalEventElement extends TestCaseInstanceElement { public String signalName; diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceMemo.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceMemo.java index 21c43ad..d1dcbcb 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceMemo.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TestCaseInstanceMemo.java @@ -59,7 +59,7 @@ void apply(Record record) { break; } } catch (RuntimeException e) { - System.err.println(String.format("failed to process record: %s", record)); + System.err.printf("failed to process record: %s%n", record); } } diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TimerEventHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TimerEventHandler.java index 54de4bd..4e21ab2 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/TimerEventHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/TimerEventHandler.java @@ -11,6 +11,9 @@ import io.camunda.zeebe.process.test.assertions.BpmnAssert; import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +/** + * Fluent API to handle timer catch events. + */ public class TimerEventHandler { private final TimerEventElement element; diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/api/UserTaskHandler.java b/impl-8/src/main/java/org/camunda/community/bpmndt/api/UserTaskHandler.java index 8658f64..ca83a09 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/api/UserTaskHandler.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/api/UserTaskHandler.java @@ -14,6 +14,9 @@ import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; import io.camunda.zeebe.protocol.Protocol; +/** + * Fluent API to handle user tasks. + */ public class UserTaskHandler { private final UserTaskElement element; diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/cmd/BuildTestCaseContext.java b/impl-8/src/main/java/org/camunda/community/bpmndt/cmd/BuildTestCaseContext.java index d8082a8..f694374 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/cmd/BuildTestCaseContext.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/cmd/BuildTestCaseContext.java @@ -20,6 +20,7 @@ import org.camunda.community.bpmndt.strategy.JobStrategy; import org.camunda.community.bpmndt.strategy.MessageBoundaryEventStrategy; import org.camunda.community.bpmndt.strategy.MessageEventStrategy; +import org.camunda.community.bpmndt.strategy.OutboundConnectorStrategy; import org.camunda.community.bpmndt.strategy.SignalBoundaryEventStrategy; import org.camunda.community.bpmndt.strategy.SignalEventStrategy; import org.camunda.community.bpmndt.strategy.TimerBoundaryEventStrategy; @@ -116,6 +117,8 @@ private GeneratorStrategy createStrategy(BpmnElement element) { return new MessageBoundaryEventStrategy(element); case MESSAGE_CATCH: return new MessageEventStrategy(element); + case OUTBOUND_CONNECTOR: + return new OutboundConnectorStrategy(element); case SERVICE_TASK: return new JobStrategy(element); case SIGNAL_BOUNDARY: diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/CallActivityStrategy.java b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/CallActivityStrategy.java index c62e024..b639663 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/CallActivityStrategy.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/CallActivityStrategy.java @@ -56,16 +56,18 @@ public void initHandlerElement(MethodSpec.Builder methodBuilder) { methodBuilder.addStatement("$LElement.id = $S", literal, element.getId()); var extensionElements = element.getFlowNode().getExtensionElements(); - if (extensionElements != null) { - var calledElement = (ZeebeCalledElement) extensionElements.getUniqueChildElementByType(ZeebeCalledElement.class); - if (calledElement != null) { - if (calledElement.getProcessId() != null) { - methodBuilder.addStatement("$LElement.processId = $S", literal, calledElement.getProcessId()); - } + if (extensionElements == null) { + return; + } - methodBuilder.addStatement("$LElement.propagateAllChildVariables = $L", literal, calledElement.isPropagateAllChildVariablesEnabled()); - methodBuilder.addStatement("$LElement.propagateAllParentVariables = $L", literal, calledElement.isPropagateAllParentVariablesEnabled()); + var calledElement = (ZeebeCalledElement) extensionElements.getUniqueChildElementByType(ZeebeCalledElement.class); + if (calledElement != null) { + if (calledElement.getProcessId() != null) { + methodBuilder.addStatement("$LElement.processId = $S", literal, calledElement.getProcessId()); } + + methodBuilder.addStatement("$LElement.propagateAllChildVariables = $L", literal, calledElement.isPropagateAllChildVariablesEnabled()); + methodBuilder.addStatement("$LElement.propagateAllParentVariables = $L", literal, calledElement.isPropagateAllParentVariablesEnabled()); } } } diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/DefaultStrategy.java b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/DefaultStrategy.java index c474197..c98ac5a 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/DefaultStrategy.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/DefaultStrategy.java @@ -6,6 +6,7 @@ import org.camunda.community.bpmndt.api.CustomMultiInstanceHandler; import org.camunda.community.bpmndt.api.JobHandler; import org.camunda.community.bpmndt.api.MessageEventHandler; +import org.camunda.community.bpmndt.api.OutboundConnectorHandler; import org.camunda.community.bpmndt.api.SignalEventHandler; import org.camunda.community.bpmndt.api.TimerEventHandler; import org.camunda.community.bpmndt.api.UserTaskHandler; @@ -22,6 +23,7 @@ public class DefaultStrategy implements GeneratorStrategy { public static final TypeName CUSTOM_MULTI_INSTANCE = TypeName.get(CustomMultiInstanceHandler.class); public static final TypeName JOB = TypeName.get(JobHandler.class); public static final TypeName MESSAGE_EVENT = TypeName.get(MessageEventHandler.class); + public static final TypeName OUTBOUND_CONNECTOR = TypeName.get(OutboundConnectorHandler.class); public static final TypeName OTHER = TypeName.get(Void.class); public static final TypeName SIGNAL_EVENT = TypeName.get(SignalEventHandler.class); public static final TypeName TIMER_EVENT = TypeName.get(TimerEventHandler.class); diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/JobStrategy.java b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/JobStrategy.java index 4860dd8..c684719 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/JobStrategy.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/JobStrategy.java @@ -45,15 +45,17 @@ public void initHandlerElement(MethodSpec.Builder methodBuilder) { methodBuilder.addStatement("$LElement.id = $S", literal, element.getId()); var extensionElements = element.getFlowNode().getExtensionElements(); - if (extensionElements != null) { - var taskDefinition = (ZeebeTaskDefinition) extensionElements.getUniqueChildElementByType(ZeebeTaskDefinition.class); - if (taskDefinition != null) { - if (taskDefinition.getRetries() != null) { - methodBuilder.addStatement("$LElement.retries = $S", literal, taskDefinition.getRetries()); - } - if (taskDefinition.getType() != null) { - methodBuilder.addStatement("$LElement.type = $S", literal, taskDefinition.getType()); - } + if (extensionElements == null) { + return; + } + + var taskDefinition = (ZeebeTaskDefinition) extensionElements.getUniqueChildElementByType(ZeebeTaskDefinition.class); + if (taskDefinition != null) { + if (taskDefinition.getRetries() != null) { + methodBuilder.addStatement("$LElement.retries = $S", literal, taskDefinition.getRetries()); + } + if (taskDefinition.getType() != null) { + methodBuilder.addStatement("$LElement.type = $S", literal, taskDefinition.getType()); } } } diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/MessageEventStrategy.java b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/MessageEventStrategy.java index abfa107..1800637 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/MessageEventStrategy.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/MessageEventStrategy.java @@ -57,9 +57,12 @@ public void initHandlerElement(MethodSpec.Builder methodBuilder) { } if (message != null) { - var subscription = (ZeebeSubscription) message.getExtensionElements().getUniqueChildElementByType(ZeebeSubscription.class); - if (subscription != null && subscription.getCorrelationKey() != null) { - methodBuilder.addStatement("$LElement.correlationKey = $S", literal, subscription.getCorrelationKey()); + var extensionElements = message.getExtensionElements(); + if (extensionElements != null) { + var subscription = (ZeebeSubscription) extensionElements.getUniqueChildElementByType(ZeebeSubscription.class); + if (subscription != null && subscription.getCorrelationKey() != null) { + methodBuilder.addStatement("$LElement.correlationKey = $S", literal, subscription.getCorrelationKey()); + } } if (message.getName() != null) { diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/OutboundConnectorStrategy.java b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/OutboundConnectorStrategy.java new file mode 100644 index 0000000..33ad4e0 --- /dev/null +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/OutboundConnectorStrategy.java @@ -0,0 +1,106 @@ +package org.camunda.community.bpmndt.strategy; + +import java.util.HashMap; + +import org.camunda.community.bpmndt.api.TestCaseInstanceElement.OutboundConnectorElement; +import org.camunda.community.bpmndt.model.BpmnElement; +import org.camunda.community.bpmndt.model.BpmnElementType; +import org.camunda.community.bpmndt.model.BpmnEventSupport; + +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; + +import io.camunda.zeebe.model.bpmn.instance.BoundaryEvent; +import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeHeader; +import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeInput; +import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeIoMapping; +import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeOutput; +import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeTaskDefinition; +import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeTaskHeaders; + +public class OutboundConnectorStrategy extends DefaultHandlerStrategy { + + public OutboundConnectorStrategy(BpmnElement element) { + super(element); + } + + @Override + public TypeName getHandlerType() { + return OUTBOUND_CONNECTOR; + } + + @Override + public void initHandler(MethodSpec.Builder methodBuilder) { + super.initHandler(methodBuilder); + + if (!element.hasNext()) { + return; + } + + var next = element.getNext(); + if (!next.getType().isBoundaryEvent()) { + return; + } + + if (next.getType() == BpmnElementType.ERROR_BOUNDARY) { + var event = next.getFlowNode(BoundaryEvent.class); + var eventSupport = new BpmnEventSupport(event); + + var errorCode = eventSupport.getErrorCode(); + methodBuilder.addStatement("$L.throwBpmnError($S, $S)", literal, errorCode, "error message, generated by BPMNDT"); + } + } + + @Override + public void initHandlerElement(MethodSpec.Builder methodBuilder) { + methodBuilder.addCode("\n// $L: $L\n", element.getTypeName(), element.getId()); + methodBuilder.addStatement("$T $LElement = new $T()", OutboundConnectorElement.class, literal, OutboundConnectorElement.class); + methodBuilder.addStatement("$LElement.id = $S", literal, element.getId()); + + var extensionElements = element.getFlowNode().getExtensionElements(); + if (extensionElements == null) { + return; + } + + var taskDefinition = (ZeebeTaskDefinition) extensionElements.getUniqueChildElementByType(ZeebeTaskDefinition.class); + if (taskDefinition != null) { + if (taskDefinition.getType() != null) { + methodBuilder.addStatement("$LElement.taskDefinitionType = $S", literal, taskDefinition.getType()); + } + } + + var ioMapping = (ZeebeIoMapping) extensionElements.getUniqueChildElementByType(ZeebeIoMapping.class); + if (ioMapping != null) { + var inputs = ioMapping.getInputs(); + if (inputs != null && !inputs.isEmpty()) { + methodBuilder.addStatement("$LElement.inputs = new $T()", literal, ParameterizedTypeName.get(HashMap.class, String.class, String.class)); + + for (ZeebeInput input : inputs) { + methodBuilder.addStatement("$LElement.inputs.put($S, $S)", literal, input.getTarget(), input.getSource()); + } + } + + var outputs = ioMapping.getOutputs(); + if (outputs != null && !outputs.isEmpty()) { + methodBuilder.addStatement("$LElement.outputs = new $T()", literal, ParameterizedTypeName.get(HashMap.class, String.class, String.class)); + + for (ZeebeOutput output : outputs) { + methodBuilder.addStatement("$LElement.outputs.put($S, $S)", literal, output.getTarget(), output.getSource()); + } + } + } + + var taskHeaders = (ZeebeTaskHeaders) extensionElements.getUniqueChildElementByType(ZeebeTaskHeaders.class); + if (taskHeaders != null) { + var headers = taskHeaders.getHeaders(); + if (headers != null && !headers.isEmpty()) { + methodBuilder.addStatement("$LElement.taskHeaders = new $T()", literal, ParameterizedTypeName.get(HashMap.class, String.class, String.class)); + + for (ZeebeHeader header : headers) { + methodBuilder.addStatement("$LElement.taskHeaders.put($S, $S)", literal, header.getKey(), header.getValue()); + } + } + } + } +} diff --git a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/UserTaskStrategy.java b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/UserTaskStrategy.java index 5ce2537..bcc10d0 100644 --- a/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/UserTaskStrategy.java +++ b/impl-8/src/main/java/org/camunda/community/bpmndt/strategy/UserTaskStrategy.java @@ -9,7 +9,6 @@ import com.squareup.javapoet.TypeName; import io.camunda.zeebe.model.bpmn.instance.BoundaryEvent; -import io.camunda.zeebe.model.bpmn.instance.UserTask; import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeAssignmentDefinition; import io.camunda.zeebe.model.bpmn.instance.zeebe.ZeebeTaskSchedule; @@ -54,30 +53,31 @@ public void initHandlerElement(MethodSpec.Builder methodBuilder) { methodBuilder.addStatement("$T $LElement = new $T()", UserTaskElement.class, literal, UserTaskElement.class); methodBuilder.addStatement("$LElement.id = $S", literal, element.getId()); - var userTask = element.getFlowNode(UserTask.class); - var extensionElements = userTask.getExtensionElements(); - if (extensionElements != null) { - var assignmentDefinition = (ZeebeAssignmentDefinition) extensionElements.getUniqueChildElementByType(ZeebeAssignmentDefinition.class); - if (assignmentDefinition != null) { - if (assignmentDefinition.getAssignee() != null) { - methodBuilder.addStatement("$LElement.assignee = $S", literal, assignmentDefinition.getAssignee()); - } - if (assignmentDefinition.getCandidateGroups() != null) { - methodBuilder.addStatement("$LElement.candidateGroups = $S", literal, assignmentDefinition.getCandidateGroups()); - } - if (assignmentDefinition.getCandidateUsers() != null) { - methodBuilder.addStatement("$LElement.candidateUsers = $S", literal, assignmentDefinition.getCandidateUsers()); - } + var extensionElements = element.getFlowNode().getExtensionElements(); + if (extensionElements == null) { + return; + } + + var assignmentDefinition = (ZeebeAssignmentDefinition) extensionElements.getUniqueChildElementByType(ZeebeAssignmentDefinition.class); + if (assignmentDefinition != null) { + if (assignmentDefinition.getAssignee() != null) { + methodBuilder.addStatement("$LElement.assignee = $S", literal, assignmentDefinition.getAssignee()); + } + if (assignmentDefinition.getCandidateGroups() != null) { + methodBuilder.addStatement("$LElement.candidateGroups = $S", literal, assignmentDefinition.getCandidateGroups()); } + if (assignmentDefinition.getCandidateUsers() != null) { + methodBuilder.addStatement("$LElement.candidateUsers = $S", literal, assignmentDefinition.getCandidateUsers()); + } + } - var taskSchedule = (ZeebeTaskSchedule) extensionElements.getUniqueChildElementByType(ZeebeTaskSchedule.class); - if (taskSchedule != null) { - if (taskSchedule.getDueDate() != null) { - methodBuilder.addStatement("$LElement.dueDate = $S", literal, taskSchedule.getDueDate()); - } - if (taskSchedule.getFollowUpDate() != null) { - methodBuilder.addStatement("$LElement.followUpDate = $S", literal, taskSchedule.getFollowUpDate()); - } + var taskSchedule = (ZeebeTaskSchedule) extensionElements.getUniqueChildElementByType(ZeebeTaskSchedule.class); + if (taskSchedule != null) { + if (taskSchedule.getDueDate() != null) { + methodBuilder.addStatement("$LElement.dueDate = $S", literal, taskSchedule.getDueDate()); + } + if (taskSchedule.getFollowUpDate() != null) { + methodBuilder.addStatement("$LElement.followUpDate = $S", literal, taskSchedule.getFollowUpDate()); } } } diff --git a/impl-8/src/test/java/org/camunda/community/bpmndt/GeneratorTest.java b/impl-8/src/test/java/org/camunda/community/bpmndt/GeneratorTest.java index c220352..1012661 100644 --- a/impl-8/src/test/java/org/camunda/community/bpmndt/GeneratorTest.java +++ b/impl-8/src/test/java/org/camunda/community/bpmndt/GeneratorTest.java @@ -132,6 +132,7 @@ void testGenerate() { assertThat(isFile.test("org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java")).isTrue(); assertThat(isFile.test("org/camunda/community/bpmndt/api/JobHandler.java")).isTrue(); assertThat(isFile.test("org/camunda/community/bpmndt/api/MessageEventHandler.java")).isTrue(); + assertThat(isFile.test("org/camunda/community/bpmndt/api/OutboundConnectorHandler.java")).isTrue(); assertThat(isFile.test("org/camunda/community/bpmndt/api/SignalEventHandler.java")).isTrue(); assertThat(isFile.test("org/camunda/community/bpmndt/api/SimulateSubProcessResource.java")).isTrue(); assertThat(isFile.test("org/camunda/community/bpmndt/api/TestCaseExecutor.java")).isTrue(); diff --git a/impl-8/src/test/java/org/camunda/community/bpmndt/api/OutboundConnectorErrorTest.java b/impl-8/src/test/java/org/camunda/community/bpmndt/api/OutboundConnectorErrorTest.java new file mode 100644 index 0000000..2d8e7a0 --- /dev/null +++ b/impl-8/src/test/java/org/camunda/community/bpmndt/api/OutboundConnectorErrorTest.java @@ -0,0 +1,103 @@ +package org.camunda.community.bpmndt.api; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +import org.camunda.community.bpmndt.test.TestPaths; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.camunda.zeebe.process.test.api.ZeebeTestEngine; +import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +import io.camunda.zeebe.process.test.extension.ZeebeProcessTest; + +@ZeebeProcessTest +class OutboundConnectorErrorTest { + + @RegisterExtension + TestCase tc = new TestCase(); + + ZeebeTestEngine engine; + + private OutboundConnectorHandler handler; + + @BeforeEach + void setUp() { + handler = new OutboundConnectorHandler("outboundConnector"); + } + + @Test + void testThrowBpmnError() { + handler.throwBpmnError("ADVANCED_ERROR", "test error message"); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testThrowBpmnErrorWithVariables() { + handler + .withVariable("x", "test") + .withVariable("y", 1) + .withVariable("z", true) + .throwBpmnError("ADVANCED_ERROR", "test error message"); + + tc.createExecutor(engine).verify(piAssert -> { + piAssert.hasVariableWithValue("x", "test"); + piAssert.hasVariableWithValue("y", 1); + piAssert.hasVariableWithValue("z", true); + + // TODO map error message via FEEL to verify throw error command included the specified error message + // but it seems that it is currently not possible!? + // piAssert.hasVariableWithValue("errorMessage", "test error message"); + + piAssert.isCompleted(); + }).execute(); + } + + @Test + void testExecuteAction() { + handler.execute((client, jobKey) -> client.newThrowErrorCommand(jobKey).errorCode("ADVANCED_ERROR").send()); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + private class TestCase extends AbstractJUnit5TestCase { + + @Override + protected void execute(TestCaseInstance instance, long processInstanceKey) { + instance.hasPassed(processInstanceKey, "startEvent"); + instance.isWaitingAt(processInstanceKey, "outboundConnector"); + instance.apply(processInstanceKey, handler); + instance.hasTerminated(processInstanceKey, "outboundConnector"); + instance.hasPassed(processInstanceKey, "errorBoundaryEvent"); + instance.hasPassed(processInstanceKey, "endEvent"); + instance.isCompleted(processInstanceKey); + } + + @Override + public String getBpmnProcessId() { + return "outboundConnectorError"; + } + + @Override + protected InputStream getBpmnResource() { + try { + return Files.newInputStream(TestPaths.advanced("outboundConnectorError.bpmn")); + } catch (IOException e) { + return null; + } + } + + @Override + public String getStart() { + return "startEvent"; + } + + @Override + public String getEnd() { + return "endEvent"; + } + } +} diff --git a/impl-8/src/test/java/org/camunda/community/bpmndt/api/OutboundConnectorTest.java b/impl-8/src/test/java/org/camunda/community/bpmndt/api/OutboundConnectorTest.java new file mode 100644 index 0000000..b497f83 --- /dev/null +++ b/impl-8/src/test/java/org/camunda/community/bpmndt/api/OutboundConnectorTest.java @@ -0,0 +1,245 @@ +package org.camunda.community.bpmndt.api; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import java.util.HashMap; +import java.util.Map; + +import org.camunda.community.bpmndt.api.TestCaseInstanceElement.OutboundConnectorElement; +import org.camunda.community.bpmndt.test.TestPaths; +import org.camunda.community.bpmndt.test.TestVariables; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.camunda.zeebe.process.test.api.ZeebeTestEngine; +import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +import io.camunda.zeebe.process.test.extension.ZeebeProcessTest; + +@ZeebeProcessTest +class OutboundConnectorTest { + + @RegisterExtension + TestCase tc = new TestCase(); + + ZeebeTestEngine engine; + + private OutboundConnectorHandler handler; + + @BeforeEach + void setUp() { + var element = new OutboundConnectorElement(); + element.id = "outboundConnector"; + element.inputs = Map.of( + "authentication.type", "noAuth", + "method", "GET", + "url", "=\"https://example.org\"", + "headers", "=headers", + "queryParameters", "=queryParameters", + "connectionTimeoutInSeconds", "20" + ); + element.outputs = Map.of("x", "y"); + element.taskDefinitionType = "io.camunda:http-json:1"; + element.taskHeaders = Map.of( + "resultVariable", "responseBody", + "resultExpression", "={}", + "errorExpression", "if error.code = \"400\" then\n" + + " bpmnError(\"400\", \"bad request\")\n" + + "else\n" + + " null", + "retries", "3", + "retryBackoff", "PT1H" + ); + + handler = new OutboundConnectorHandler(element); + } + + @Test + void testExecute() { + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testVerify() { + handler.verify(processInstanceAssert -> { + processInstanceAssert.hasVariableWithValue("authentication", Map.of("type", "noAuth")); + processInstanceAssert.hasVariableWithValue("method", "GET"); + processInstanceAssert.hasVariableWithValue("url", "https://example.org"); + processInstanceAssert.hasVariableWithValue("headers", null); + processInstanceAssert.hasVariableWithValue("queryParameters", null); + processInstanceAssert.hasVariableWithValue("connectionTimeoutInSeconds", "20"); + }); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testVerifyInputMapping() { + handler.verifyInputMapping(inputMapping -> assertThat(inputMapping).containsEntry("x", "y")); + + assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + + handler.verifyInputMapping(inputMapping -> { + assertThat(inputMapping).containsEntry("authentication.type", "noAuth"); + assertThat(inputMapping).containsEntry("method", "GET"); + assertThat(inputMapping).containsEntry("url", "=\"https://example.org\""); + assertThat(inputMapping).containsEntry("headers", "=headers"); + assertThat(inputMapping).containsEntry("queryParameters", "=queryParameters"); + assertThat(inputMapping).containsEntry("connectionTimeoutInSeconds", "20"); + }); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testVerifyOutputMapping() { + handler.verifyOutputMapping(outputMapping -> assertThat(outputMapping).containsEntry("a", "b")); + + assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + + handler.verifyOutputMapping(outputMapping -> assertThat(outputMapping).containsEntry("x", "y")); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testVerifyRetries() { + handler.verifyRetries(2); + + var e = assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + assertThat(e).hasMessageThat().contains("but was 3"); + assertThat(e).hasMessageThat().contains("retry count of 2"); + + handler.verifyRetries(3); + + handler.verifyRetries(retries -> assertThat(retries).isEqualTo(2)); + + assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + + handler.verifyRetries(retries -> assertThat(retries).isEqualTo(3)); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testVerifyTaskDefinitionType() { + handler.verifyTaskDefinitionType("wrong type"); + + var e = assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + assertThat(e).hasMessageThat().contains("'wrong type'"); + assertThat(e).hasMessageThat().contains("'io.camunda:http-json:1'"); + + handler.verifyTaskDefinitionType("io.camunda:http-json:1"); + + handler.verifyTaskDefinitionType(type -> assertThat(type).isEqualTo("wrong type")); + + assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + + handler.verifyTaskDefinitionType(type -> assertThat(type).isEqualTo("io.camunda:http-json:1")); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testVerifyTaskHeaders() { + handler.verifyTaskHeaders(taskHeaders -> assertThat(taskHeaders).containsEntry("resultVariable", null)); + + assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + + handler.verifyTaskHeaders(taskHeaders -> { + assertThat(taskHeaders).containsEntry("resultVariable", "responseBody"); + assertThat(taskHeaders).containsEntry("resultExpression", "={}"); + assertThat(taskHeaders).containsKey("errorExpression"); + assertThat(taskHeaders.get("errorExpression")).contains("bpmnError(\"400\", \"bad request\")"); + assertThat(taskHeaders).containsEntry("retries", "3"); + assertThat(taskHeaders).containsEntry("retryBackoff", "PT1H"); + }); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testExecuteAction() { + handler.execute((client, jobKey) -> client.newCompleteCommand(jobKey).send()); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } + + @Test + void testCompleteWithVariables() { + var variables = new TestVariables(); + variables.setX("test"); + variables.setY(1); + variables.setZ(true); + + handler.withVariables(variables).complete(); + + tc.createExecutor(engine).verify(piAssert -> { + piAssert.isCompleted(); + + piAssert.hasVariableWithValue("x", "test"); + piAssert.hasVariableWithValue("y", 1); + piAssert.hasVariableWithValue("z", true); + }).execute(); + } + + @Test + void testCompleteWithVariableMap() { + var variableMap = new HashMap(); + variableMap.put("y", 1); + variableMap.put("z", true); + + handler + .withVariable("x", "test") + .withVariableMap(variableMap) + .complete(); + + tc.createExecutor(engine).verify(piAssert -> { + piAssert.isCompleted(); + + piAssert.hasVariableWithValue("x", "test"); + piAssert.hasVariableWithValue("y", 1); + piAssert.hasVariableWithValue("z", true); + }).execute(); + } + + private class TestCase extends AbstractJUnit5TestCase { + + @Override + protected void execute(TestCaseInstance instance, long processInstanceKey) { + instance.hasPassed(processInstanceKey, "startEvent"); + instance.apply(processInstanceKey, handler); + instance.hasPassed(processInstanceKey, "outboundConnector"); + instance.hasPassed(processInstanceKey, "endEvent"); + instance.isCompleted(processInstanceKey); + } + + @Override + public String getBpmnProcessId() { + return "simpleOutboundConnector"; + } + + @Override + protected InputStream getBpmnResource() { + try { + return Files.newInputStream(TestPaths.simple("simpleOutboundConnector.bpmn")); + } catch (IOException e) { + return null; + } + } + + @Override + public String getStart() { + return "startEvent"; + } + + @Override + public String getEnd() { + return "endEvent"; + } + } +} diff --git a/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskErrorTest.java b/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskErrorTest.java index 4bae00b..f7dd7dd 100644 --- a/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskErrorTest.java +++ b/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskErrorTest.java @@ -67,18 +67,19 @@ void testThrowBpmnError() { private class TestCase extends AbstractJUnit5TestCase { @Override - public String getBpmnProcessId() { - return "serviceTaskError"; - } - - @Override - public String getEnd() { - return "endEvent"; + protected void execute(TestCaseInstance instance, long processInstanceKey) { + instance.hasPassed(processInstanceKey, "startEvent"); + instance.isWaitingAt(processInstanceKey, "serviceTask"); + instance.apply(processInstanceKey, handler); + instance.hasTerminated(processInstanceKey, "serviceTask"); + instance.hasPassed(processInstanceKey, "errorBoundaryEvent"); + instance.hasPassed(processInstanceKey, "endEvent"); + instance.isCompleted(processInstanceKey); } @Override - public String getStart() { - return "startEvent"; + public String getBpmnProcessId() { + return "serviceTaskError"; } @Override @@ -91,13 +92,13 @@ protected InputStream getBpmnResource() { } @Override - protected void execute(TestCaseInstance instance, long processInstanceKey) { - instance.hasPassed(processInstanceKey, "startEvent"); - instance.isWaitingAt(processInstanceKey, "serviceTask"); - instance.apply(processInstanceKey, handler); - instance.hasPassed(processInstanceKey, "errorBoundaryEvent"); - instance.hasPassed(processInstanceKey, "endEvent"); - instance.isCompleted(processInstanceKey); + public String getStart() { + return "startEvent"; + } + + @Override + public String getEnd() { + return "endEvent"; } } } diff --git a/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskTest.java b/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskTest.java index 493135a..2b60b02 100644 --- a/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskTest.java +++ b/impl-8/src/test/java/org/camunda/community/bpmndt/api/ServiceTaskTest.java @@ -140,7 +140,7 @@ void testVerifyRetries() { handler.verifyRetries(2); try (var ignored = workerBuilder.open()) { - AssertionError e = assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + var e = assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); assertThat(e).hasMessageThat().contains("but was 3"); assertThat(e).hasMessageThat().contains("retry count of 2"); } @@ -186,7 +186,7 @@ void testVerifyType() { handler.verifyType("wrong type"); try (var ignored = workerBuilder.open()) { - AssertionError e = assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); + var e = assertThrows(AssertionError.class, () -> tc.createExecutor(engine).execute()); assertThat(e).hasMessageThat().contains("'wrong type'"); assertThat(e).hasMessageThat().contains("'serviceTaskType'"); } @@ -220,7 +220,7 @@ void testVerifyTypeExpression() { @Test void testExecuteAction() { - handler.execute((client, job) -> client.newCompleteCommand(job).send()); + handler.execute((client, jobKey) -> client.newCompleteCommand(jobKey).send()); tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); } diff --git a/integration-tests-8/advanced/src/main/resources/outboundConnectorError.bpmn b/integration-tests-8/advanced/src/main/resources/outboundConnectorError.bpmn new file mode 100644 index 0000000..e889b36 --- /dev/null +++ b/integration-tests-8/advanced/src/main/resources/outboundConnectorError.bpmn @@ -0,0 +1,83 @@ + + + + + + + + startEvent + outboundConnector + errorBoundaryEvent + endEvent + + + + + + Flow_1tlvfpk + + + + Flow_1u549lq + + + + + + + + + + + + + + + + + Flow_1tlvfpk + Flow_1u549lq + + + Flow_0obak9w + + + + Flow_0obak9w + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests-8/simple.robot b/integration-tests-8/simple.robot index 698f6cd..d68d188 100644 --- a/integration-tests-8/simple.robot +++ b/integration-tests-8/simple.robot @@ -80,6 +80,7 @@ Assert Test Code Generation Should contain ${result.stdout} Found BPMN file: simpleMessageEndEvent.bpmn Should contain ${result.stdout} Found BPMN file: simpleMessageStartEvent.bpmn Should contain ${result.stdout} Found BPMN file: simpleMessageThrowEvent.bpmn + Should contain ${result.stdout} Found BPMN file: simpleOutboundConnector.bpmn Should contain ${result.stdout} Found BPMN file: simpleReceiveTask.bpmn Should contain ${result.stdout} Found BPMN file: simpleScriptTask.bpmn Should contain ${result.stdout} Found BPMN file: simpleSendTask.bpmn @@ -137,6 +138,7 @@ Assert Test Code Generation Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simplemessageendevent/TC_startEvent__messageEndEvent.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simplemessagestartevent/TC_messageStartEvent__endEvent.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simplemessagethrowevent/TC_startEvent__endEvent.java + Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simpleoutboundconnector/TC_startEvent__endEvent.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simplereceivetask/TC_startEvent__endEvent.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simplescripttask/TC_startEvent__endEvent.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/generated/simplesendtask/TC_startEvent__endEvent.java @@ -163,6 +165,7 @@ Assert Test Code Generation File should exist ${testSources}/generated/simplemessageendevent/TC_startEvent__messageEndEvent.java File should exist ${testSources}/generated/simplemessagestartevent/TC_messageStartEvent__endEvent.java File should exist ${testSources}/generated/simplemessagethrowevent/TC_startEvent__endEvent.java + File should exist ${testSources}/generated/simpleoutboundconnector/TC_startEvent__endEvent.java File should exist ${testSources}/generated/simplereceivetask/TC_startEvent__endEvent.java File should exist ${testSources}/generated/simplescripttask/TC_startEvent__endEvent.java File should exist ${testSources}/generated/simplesendtask/TC_startEvent__endEvent.java @@ -185,6 +188,7 @@ Assert Test Code Generation Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/JobHandler.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/MessageEventHandler.java + Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/OutboundConnectorHandler.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/SignalEventHandler.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/SimulateSubProcessResource.java Should contain ${result.stdout} Writing file: ${buildDir}/bpmndt/org/camunda/community/bpmndt/api/TestCaseInstance.java @@ -200,6 +204,7 @@ Assert Test Code Generation File should exist ${testSources}/org/camunda/community/bpmndt/api/CustomMultiInstanceHandler.java File should exist ${testSources}/org/camunda/community/bpmndt/api/JobHandler.java File should exist ${testSources}/org/camunda/community/bpmndt/api/MessageEventHandler.java + File should exist ${testSources}/org/camunda/community/bpmndt/api/OutboundConnectorHandler.java File should exist ${testSources}/org/camunda/community/bpmndt/api/SignalEventHandler.java File should exist ${testSources}/org/camunda/community/bpmndt/api/SimulateSubProcessResource.java File should exist ${testSources}/org/camunda/community/bpmndt/api/TestCaseInstance.java diff --git a/integration-tests-8/simple/src/main/resources/simpleOutboundConnector.bpmn b/integration-tests-8/simple/src/main/resources/simpleOutboundConnector.bpmn new file mode 100644 index 0000000..ffb1486 --- /dev/null +++ b/integration-tests-8/simple/src/main/resources/simpleOutboundConnector.bpmn @@ -0,0 +1,67 @@ + + + + + + + + startEvent + outboundConnector + endEvent + + + + + + Flow_0quynq1 + + + + + + + + + + + + + + + + + + + + + + Flow_0quynq1 + Flow_0ngn3ir + + + Flow_0ngn3ir + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/integration-tests-8/simple/src/test/java/org/example/it/SimpleOutboundConnectorTest.java b/integration-tests-8/simple/src/test/java/org/example/it/SimpleOutboundConnectorTest.java new file mode 100644 index 0000000..694681a --- /dev/null +++ b/integration-tests-8/simple/src/test/java/org/example/it/SimpleOutboundConnectorTest.java @@ -0,0 +1,58 @@ +package org.example.it; + +import static com.google.common.truth.Truth.assertThat; + +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import generated.simpleoutboundconnector.TC_startEvent__endEvent; +import io.camunda.zeebe.process.test.api.ZeebeTestEngine; +import io.camunda.zeebe.process.test.assertions.ProcessInstanceAssert; +import io.camunda.zeebe.process.test.extension.ZeebeProcessTest; + +@ZeebeProcessTest +class SimpleOutboundConnectorTest { + + @RegisterExtension + TC_startEvent__endEvent tc = new TC_startEvent__endEvent(); + + ZeebeTestEngine engine; + + @Test + void testExecute() { + tc.handleOutboundConnector() + .verify(processInstanceAssert -> { + processInstanceAssert.hasVariableWithValue("authentication", Map.of("type", "noAuth")); + processInstanceAssert.hasVariableWithValue("method", "GET"); + processInstanceAssert.hasVariableWithValue("url", "https://example.org"); + processInstanceAssert.hasVariableWithValue("headers", null); + processInstanceAssert.hasVariableWithValue("queryParameters", null); + processInstanceAssert.hasVariableWithValue("connectionTimeoutInSeconds", "20"); + }) + .verifyInputMapping(inputMapping -> { + assertThat(inputMapping).containsEntry("authentication.type", "noAuth"); + assertThat(inputMapping).containsEntry("method", "GET"); + assertThat(inputMapping).containsEntry("url", "=\"https://example.org\""); + assertThat(inputMapping).containsEntry("headers", "=headers"); + assertThat(inputMapping).containsEntry("queryParameters", "=queryParameters"); + assertThat(inputMapping).containsEntry("connectionTimeoutInSeconds", "20"); + }) + .verifyOutputMapping(outputMapping -> assertThat(outputMapping).isNull()) + .verifyRetries(3) + .verifyRetries(retries -> assertThat(retries).isEqualTo(3)) + .verifyTaskDefinitionType("io.camunda:http-json:1") + .verifyTaskDefinitionType(type -> assertThat(type).isEqualTo("io.camunda:http-json:1")) + .verifyTaskHeaders(taskHeaders -> { + assertThat(taskHeaders).containsEntry("resultVariable", "responseBody"); + assertThat(taskHeaders).containsEntry("resultExpression", "={}"); + assertThat(taskHeaders).containsKey("errorExpression"); + assertThat(taskHeaders.get("errorExpression")).contains("bpmnError(\"400\", \"bad request\")"); + assertThat(taskHeaders).containsEntry("retries", "3"); + assertThat(taskHeaders).containsEntry("retryBackoff", "PT1H"); + }); + + tc.createExecutor(engine).verify(ProcessInstanceAssert::isCompleted).execute(); + } +} diff --git a/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnElementType.java b/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnElementType.java index dea5b8c..ef6778a 100644 --- a/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnElementType.java +++ b/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnElementType.java @@ -15,6 +15,7 @@ public enum BpmnElementType { LINK_THROW, MESSAGE_BOUNDARY, MESSAGE_CATCH, + OUTBOUND_CONNECTOR, SERVICE_TASK, SIGNAL_BOUNDARY, SIGNAL_CATCH, diff --git a/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnExtension.java b/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnExtension.java index 24f1eaf..14ce090 100644 --- a/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnExtension.java +++ b/model-8/src/main/java/org/camunda/community/bpmndt/model/BpmnExtension.java @@ -33,6 +33,8 @@ public final class BpmnExtension { public static final String MODELER_EXECUTION_PLATFORM = "Camunda Cloud"; public static final String MODELER_EXECUTION_PLATFORM_ATTRIBUTE = "executionPlatform"; public static final String MODELER_NS = "http://camunda.org/schema/modeler/1.0"; + public static final String ZEEBE_MODELER_TEMPLATE_ATTRIBUTE = "modelerTemplate"; + public static final String ZEEBE_NS = "http://camunda.org/schema/zeebe/1.0"; public static final String NS = "http://camunda.org/schema/extension/bpmn-driven-testing"; diff --git a/model-8/src/main/java/org/camunda/community/bpmndt/model/TestCasesImpl.java b/model-8/src/main/java/org/camunda/community/bpmndt/model/TestCasesImpl.java index a61572f..23c1f4a 100644 --- a/model-8/src/main/java/org/camunda/community/bpmndt/model/TestCasesImpl.java +++ b/model-8/src/main/java/org/camunda/community/bpmndt/model/TestCasesImpl.java @@ -214,14 +214,22 @@ private void handleIntermediateThrowEvent(BpmnElementImpl element) { } private void handleTask(BpmnElementImpl element) { - var extensionElements = element.getFlowNode().getExtensionElements(); + var flowNode = element.getFlowNode(); + + var extensionElements = flowNode.getExtensionElements(); var taskDefinition = (ZeebeTaskDefinition) extensionElements.getUniqueChildElementByType(ZeebeTaskDefinition.class); - if (taskDefinition != null) { - // handle business rule or script task with task definition as service task - element.type = BpmnElementType.SERVICE_TASK; - } else { + if (taskDefinition == null) { element.type = BpmnElementType.OTHER; + return; + } + + var modelerTemplate = flowNode.getDomElement().getAttribute(BpmnExtension.ZEEBE_NS, BpmnExtension.ZEEBE_MODELER_TEMPLATE_ATTRIBUTE); + if (modelerTemplate != null && !modelerTemplate.isBlank()) { + element.type = BpmnElementType.OUTBOUND_CONNECTOR; + } else { + // handle business rule, script or send task with task definition also as service task + element.type = BpmnElementType.SERVICE_TASK; } } @@ -276,7 +284,7 @@ private TestCase mapTestCase(TestCaseElement testCaseElement) { } else if (bpmnSupport.isEventBasedGateway(flowNodeId)) { element.type = BpmnElementType.EVENT_BASED_GATEWAY; } else if (bpmnSupport.isServiceTask(flowNodeId)) { - element.type = BpmnElementType.SERVICE_TASK; + handleTask(element); } else if (bpmnSupport.isUserTask(flowNodeId)) { element.type = BpmnElementType.USER_TASK; } else if (bpmnSupport.isIntermediateCatchEvent(flowNodeId)) { @@ -290,8 +298,7 @@ private TestCase mapTestCase(TestCaseElement testCaseElement) { } else if (bpmnSupport.isBusinessRuleTask(flowNodeId)) { handleTask(element); } else if (bpmnSupport.isSendTask(flowNodeId)) { - // handle send task as service task - element.type = BpmnElementType.SERVICE_TASK; + handleTask(element); } else if (bpmnSupport.isReceiveTask(flowNodeId)) { // handle receive task as message catch event element.type = BpmnElementType.MESSAGE_CATCH; diff --git a/model-8/src/test/java/org/camunda/community/bpmndt/model/TestCasesTest.java b/model-8/src/test/java/org/camunda/community/bpmndt/model/TestCasesTest.java index b809605..c3f0666 100644 --- a/model-8/src/test/java/org/camunda/community/bpmndt/model/TestCasesTest.java +++ b/model-8/src/test/java/org/camunda/community/bpmndt/model/TestCasesTest.java @@ -243,6 +243,17 @@ public void testNoTestCases() { assertThat(testCases.isEmpty()).isTrue(); } + @Test + public void testOutboundConnector() { + var testCases = TestCases.of(TestPaths.simple("simpleOutboundConnector.bpmn")); + assertThat(testCases.get()).hasSize(1); + + var testCase = testCases.get().get(0); + assertThat(testCase.getElements()).hasSize(3); + + assertThat(testCase.getElements().get(1).getType()).isEqualTo(BpmnElementType.OUTBOUND_CONNECTOR); + } + @Test public void testReceiveTask() { var testCases = TestCases.of(TestPaths.simple("simpleReceiveTask.bpmn")); @@ -276,6 +287,17 @@ public void testSendTask() { assertThat(testCase.getElements().get(1).getType()).isEqualTo(BpmnElementType.SERVICE_TASK); } + @Test + public void testServiceTask() { + var testCases = TestCases.of(TestPaths.simple("simpleServiceTask.bpmn")); + assertThat(testCases.get()).hasSize(1); + + var testCase = testCases.get().get(0); + assertThat(testCase.getElements()).hasSize(3); + + assertThat(testCase.getElements().get(1).getType()).isEqualTo(BpmnElementType.SERVICE_TASK); + } + @Test public void testSubProcessNested() { var testCases = TestCases.of(TestPaths.simple("simpleSubProcessNested.bpmn"));