org.camunda.bpm.engine.test.api.externaltask.ExternalTaskServiceTest.java Source code

Java tutorial

Introduction

Here is the source code for org.camunda.bpm.engine.test.api.externaltask.ExternalTaskServiceTest.java

Source

/* Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.camunda.bpm.engine.test.api.externaltask;

import org.apache.commons.lang.exception.ExceptionUtils;
import org.apache.ibatis.jdbc.RuntimeSqlException;
import org.camunda.bpm.engine.BadUserRequestException;
import org.camunda.bpm.engine.ProcessEngineException;
import org.camunda.bpm.engine.exception.NotFoundException;
import org.camunda.bpm.engine.exception.NullValueException;
import org.camunda.bpm.engine.externaltask.ExternalTask;
import org.camunda.bpm.engine.externaltask.ExternalTaskQueryBuilder;
import org.camunda.bpm.engine.externaltask.LockedExternalTask;
import org.camunda.bpm.engine.history.HistoricIncident;
import org.camunda.bpm.engine.impl.history.HistoryLevel;
import org.camunda.bpm.engine.impl.test.PluggableProcessEngineTestCase;
import org.camunda.bpm.engine.impl.util.ClockUtil;
import org.camunda.bpm.engine.runtime.ActivityInstance;
import org.camunda.bpm.engine.runtime.Incident;
import org.camunda.bpm.engine.runtime.ProcessInstance;
import org.camunda.bpm.engine.task.Task;
import org.camunda.bpm.engine.test.Deployment;
import org.camunda.bpm.engine.test.util.AssertUtil;
import org.camunda.bpm.engine.variable.VariableMap;
import org.camunda.bpm.engine.variable.Variables;
import org.joda.time.DateTime;
import org.junit.Assert;

import java.util.Date;
import java.util.List;

import static org.camunda.bpm.engine.test.util.ActivityInstanceAssert.assertThat;
import static org.camunda.bpm.engine.test.util.ActivityInstanceAssert.describeActivityInstanceTree;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.core.Is.is;
import static org.hamcrest.core.IsNull.notNullValue;

/**
 * @author Thorben Lindhauer
 *
 */
public class ExternalTaskServiceTest extends PluggableProcessEngineTestCase {

    protected static final String WORKER_ID = "aWorkerId";
    protected static final long LOCK_TIME = 10000L;
    protected static final String TOPIC_NAME = "externalTaskTopic";

    protected void setUp() throws Exception {
        ClockUtil.setCurrentTime(new Date());
    }

    protected void tearDown() throws Exception {
        ClockUtil.reset();
    }

    public void testFailOnMalformedpriorityInput() {
        try {
            repositoryService.createDeployment()
                    .addClasspathResource(
                            "org/camunda/bpm/engine/test/api/externaltask/externalTaskInvalidPriority.bpmn20.xml")
                    .deploy();
            fail("deploying a process with malformed priority should not succeed");
        } catch (ProcessEngineException e) {
            assertTextPresentIgnoreCase(
                    "value 'NOTaNumber' for attribute 'taskPriority' " + "is not a valid number", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetch() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        assertEquals(1, externalTasks.size());

        LockedExternalTask task = externalTasks.get(0);
        assertNotNull(task.getId());
        assertEquals(processInstance.getId(), task.getProcessInstanceId());
        assertEquals(processInstance.getProcessDefinitionId(), task.getProcessDefinitionId());
        assertEquals("externalTask", task.getActivityId());
        assertEquals("oneExternalTaskProcess", task.getProcessDefinitionKey());
        assertEquals(TOPIC_NAME, task.getTopicName());

        ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId())
                .getActivityInstances("externalTask")[0];

        assertEquals(activityInstance.getId(), task.getActivityInstanceId());
        assertEquals(activityInstance.getExecutionIds()[0], task.getExecutionId());

        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME), task.getLockExpirationTime());

        assertEquals(WORKER_ID, task.getWorkerId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskWithPriorityProcess.bpmn20.xml")
    public void testFetchWithPriority() {
        // given
        ProcessInstance processInstance = runtimeService
                .startProcessInstanceByKey("twoExternalTaskWithPriorityProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        assertEquals(1, externalTasks.size());

        LockedExternalTask task = externalTasks.get(0);
        assertNotNull(task.getId());
        assertEquals(processInstance.getId(), task.getProcessInstanceId());
        assertEquals(processInstance.getProcessDefinitionId(), task.getProcessDefinitionId());
        assertEquals("externalTaskWithPrio", task.getActivityId());
        assertEquals("twoExternalTaskWithPriorityProcess", task.getProcessDefinitionKey());
        assertEquals(TOPIC_NAME, task.getTopicName());
        assertEquals(7, task.getPriority());

        ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId())
                .getActivityInstances("externalTaskWithPrio")[0];

        assertEquals(activityInstance.getId(), task.getActivityInstanceId());
        assertEquals(activityInstance.getExecutionIds()[0], task.getExecutionId());

        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME), task.getLockExpirationTime());

        assertEquals(WORKER_ID, task.getWorkerId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/externalTaskPriorityProcess.bpmn20.xml")
    public void testFetchProcessWithPriority() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskWithPriorityProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(2, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(2, externalTasks.size());

        // then
        //task with no prio gets prio defined by process
        assertEquals(9, externalTasks.get(0).getPriority());
        //task with own prio overrides prio defined by process
        assertEquals(7, externalTasks.get(1).getPriority());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/externalTaskPriorityExpressionProcess.bpmn20.xml")
    public void testFetchProcessWithPriorityExpression() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskWithPriorityProcess",
                Variables.createVariables().putValue("priority", 18));

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(2, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(2, externalTasks.size());

        // then
        //task with no prio gets prio defined by process
        assertEquals(18, externalTasks.get(0).getPriority());
        //task with own prio overrides prio defined by process
        assertEquals(7, externalTasks.get(1).getPriority());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/externalTaskPriorityExpression.bpmn20.xml")
    public void testFetchWithPriorityExpression() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey(
                "twoExternalTaskWithPriorityProcess", Variables.createVariables().putValue("priority", 18));
        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        assertEquals(1, externalTasks.size());

        LockedExternalTask task = externalTasks.get(0);
        assertNotNull(task.getId());
        assertEquals(processInstance.getId(), task.getProcessInstanceId());
        assertEquals(processInstance.getProcessDefinitionId(), task.getProcessDefinitionId());
        assertEquals("externalTaskWithPrio", task.getActivityId());
        assertEquals("twoExternalTaskWithPriorityProcess", task.getProcessDefinitionKey());
        assertEquals(TOPIC_NAME, task.getTopicName());
        assertEquals(18, task.getPriority());

        ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId())
                .getActivityInstances("externalTaskWithPrio")[0];

        assertEquals(activityInstance.getId(), task.getActivityInstanceId());
        assertEquals(activityInstance.getExecutionIds()[0], task.getExecutionId());

        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME), task.getLockExpirationTime());

        assertEquals(WORKER_ID, task.getWorkerId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskWithPriorityProcess.bpmn20.xml")
    public void testFetchWithPriorityOrdering() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskWithPriorityProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(2, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        assertEquals(2, externalTasks.size());
        assertTrue(externalTasks.get(0).getPriority() > externalTasks.get(1).getPriority());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskWithPriorityProcess.bpmn20.xml")
    public void testFetchNextWithPriority() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskWithPriorityProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then the task is locked
        assertEquals(1, externalTasks.size());

        LockedExternalTask task = externalTasks.get(0);
        long firstPrio = task.getPriority();
        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME), task.getLockExpirationTime());

        // another task with next higher priority can be claimed
        externalTasks = externalTaskService.fetchAndLock(1, "anotherWorkerId", true).topic(TOPIC_NAME, LOCK_TIME)
                .execute();
        assertEquals(1, externalTasks.size());
        assertTrue(firstPrio >= externalTasks.get(0).getPriority());

        // the expiration time expires
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        //first can be claimed
        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID, true).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(1, externalTasks.size());
        assertEquals(firstPrio, externalTasks.get(0).getPriority());
    }

    @Deployment
    public void testFetchTopicSelection() {
        // given
        runtimeService.startProcessInstanceByKey("twoTopicsProcess");

        // when
        List<LockedExternalTask> topic1Tasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic("topic1", LOCK_TIME).execute();

        List<LockedExternalTask> topic2Tasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic("topic2", LOCK_TIME).execute();

        // then
        assertEquals(1, topic1Tasks.size());
        assertEquals("topic1", topic1Tasks.get(0).getTopicName());

        assertEquals(1, topic2Tasks.size());
        assertEquals("topic2", topic2Tasks.get(0).getTopicName());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchWithoutTopicName() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        try {
            externalTaskService.fetchAndLock(1, WORKER_ID).topic(null, LOCK_TIME).execute();
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("topicName is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchNullWorkerId() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        try {
            externalTaskService.fetchAndLock(1, null).topic(TOPIC_NAME, LOCK_TIME).execute();
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("workerId is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchNegativeNumberOfTasks() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        try {
            externalTaskService.fetchAndLock(-1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("maxResults is not greater than or equal to 0", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchLessTasksThanExist() {
        // given
        for (int i = 0; i < 10; i++) {
            runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        }

        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        assertEquals(5, externalTasks.size());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchNegativeLockTime() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        try {
            externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, -1L).execute();
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("lockTime is not greater than 0", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchZeroLockTime() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        try {
            externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, 0L).execute();
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("lockTime is not greater than 0", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchNoTopics() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).execute();

        // then
        assertEquals(0, tasks.size());
    }

    @Deployment
    public void testFetchVariables() {
        // given
        runtimeService.startProcessInstanceByKey("subProcessExternalTask",
                Variables.createVariables().putValue("processVar1", 42).putValue("processVar2", 43));

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).variables("processVar1", "subProcessVar", "taskVar").execute();

        // then
        LockedExternalTask task = externalTasks.get(0);
        VariableMap variables = task.getVariables();
        assertEquals(3, variables.size());

        assertEquals(42, variables.get("processVar1"));
        assertEquals(44L, variables.get("subProcessVar"));
        assertEquals(45L, variables.get("taskVar"));

    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/ExternalTaskServiceTest.testFetchVariables.bpmn20.xml")
    public void testShouldNotFetchSerializedVariables() {
        // given
        ExternalTaskCustomValue customValue = new ExternalTaskCustomValue();
        customValue.setTestValue("value1");
        runtimeService.startProcessInstanceByKey("subProcessExternalTask",
                Variables.createVariables().putValue("processVar1", customValue));

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).variables("processVar1").execute();

        // then
        LockedExternalTask task = externalTasks.get(0);
        VariableMap variables = task.getVariables();
        assertEquals(1, variables.size());

        try {
            variables.get("processVar1");
            fail("did not receive an exception although variable was serialized");
        } catch (IllegalStateException e) {
            assertEquals("Object is not deserialized.", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/ExternalTaskServiceTest.testFetchVariables.bpmn20.xml")
    public void testFetchSerializedVariables() {
        // given
        ExternalTaskCustomValue customValue = new ExternalTaskCustomValue();
        customValue.setTestValue("value1");
        runtimeService.startProcessInstanceByKey("subProcessExternalTask",
                Variables.createVariables().putValue("processVar1", customValue));

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).variables("processVar1").enableCustomObjectDeserialization()
                .execute();

        // then
        LockedExternalTask task = externalTasks.get(0);
        VariableMap variables = task.getVariables();
        assertEquals(1, variables.size());

        final ExternalTaskCustomValue receivedCustomValue = (ExternalTaskCustomValue) variables.get("processVar1");
        assertNotNull(receivedCustomValue);
        assertNotNull(receivedCustomValue.getTestValue());
        assertEquals("value1", receivedCustomValue.getTestValue());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/ExternalTaskServiceTest.testFetchVariables.bpmn20.xml")
    public void testFetchAllVariables() {
        // given
        runtimeService.startProcessInstanceByKey("subProcessExternalTask",
                Variables.createVariables().putValue("processVar1", 42).putValue("processVar2", 43));

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        LockedExternalTask task = externalTasks.get(0);
        verifyVariables(task);

        runtimeService.startProcessInstanceByKey("subProcessExternalTask",
                Variables.createVariables().putValue("processVar1", 42).putValue("processVar2", 43));

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .variables((String[]) null).execute();

        task = externalTasks.get(0);
        verifyVariables(task);

        runtimeService.startProcessInstanceByKey("subProcessExternalTask",
                Variables.createVariables().putValue("processVar1", 42).putValue("processVar2", 43));

        List<String> list = null;
        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).variables(list)
                .execute();

        task = externalTasks.get(0);
        verifyVariables(task);
    }

    private void verifyVariables(LockedExternalTask task) {
        VariableMap variables = task.getVariables();
        assertEquals(4, variables.size());

        assertEquals(42, variables.get("processVar1"));
        assertEquals(43, variables.get("processVar2"));
        assertEquals(44L, variables.get("subProcessVar"));
        assertEquals(45L, variables.get("taskVar"));
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchNonExistingVariable() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .variables("nonExistingVariable").execute();

        LockedExternalTask task = tasks.get(0);

        // then
        assertTrue(task.getVariables().isEmpty());
    }

    @Deployment
    public void testFetchMultipleTopics() {
        // given a process instance with external tasks for topics "topic1", "topic2", and "topic3"
        runtimeService.startProcessInstanceByKey("parallelExternalTaskProcess");

        // when fetching tasks for two topics
        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic("topic1", LOCK_TIME)
                .topic("topic2", LOCK_TIME * 2).execute();

        // then those two tasks are locked
        assertEquals(2, tasks.size());
        LockedExternalTask topic1Task = "topic1".equals(tasks.get(0).getTopicName()) ? tasks.get(0) : tasks.get(1);
        LockedExternalTask topic2Task = "topic2".equals(tasks.get(0).getTopicName()) ? tasks.get(0) : tasks.get(1);

        assertEquals("topic1", topic1Task.getTopicName());
        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME), topic1Task.getLockExpirationTime());

        assertEquals("topic2", topic2Task.getTopicName());
        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME * 2), topic2Task.getLockExpirationTime());

        // and the third task can still be fetched
        tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic("topic1", LOCK_TIME)
                .topic("topic2", LOCK_TIME * 2).topic("topic3", LOCK_TIME * 3).execute();

        assertEquals(1, tasks.size());

        LockedExternalTask topic3Task = tasks.get(0);
        assertEquals("topic3", topic3Task.getTopicName());
        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME * 3), topic3Task.getLockExpirationTime());
    }

    @Deployment
    public void testFetchMultipleTopicsWithVariables() {
        // given a process instance with external tasks for topics "topic1" and "topic2"
        // both have local variables "var1" and "var2"
        runtimeService.startProcessInstanceByKey("parallelExternalTaskProcess",
                Variables.createVariables().putValue("var1", 0).putValue("var2", 0));

        // when
        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic("topic1", LOCK_TIME)
                .variables("var1", "var2").topic("topic2", LOCK_TIME).variables("var1").execute();

        LockedExternalTask topic1Task = "topic1".equals(tasks.get(0).getTopicName()) ? tasks.get(0) : tasks.get(1);
        LockedExternalTask topic2Task = "topic2".equals(tasks.get(0).getTopicName()) ? tasks.get(0) : tasks.get(1);

        assertEquals("topic1", topic1Task.getTopicName());
        assertEquals("topic2", topic2Task.getTopicName());

        // then the correct variables have been fetched
        VariableMap topic1Variables = topic1Task.getVariables();
        assertEquals(2, topic1Variables.size());
        assertEquals(1L, topic1Variables.get("var1"));
        assertEquals(1L, topic1Variables.get("var2"));

        VariableMap topic2Variables = topic2Task.getVariables();
        assertEquals(1, topic2Variables.size());
        assertEquals(2L, topic2Variables.get("var1"));

    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/ExternalTaskServiceTest.testFetchMultipleTopics.bpmn20.xml")
    public void testFetchMultipleTopicsMaxTasks() {
        // given
        for (int i = 0; i < 10; i++) {
            runtimeService.startProcessInstanceByKey("parallelExternalTaskProcess");
        }

        // when
        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic("topic1", LOCK_TIME)
                .topic("topic2", LOCK_TIME).topic("topic3", LOCK_TIME).execute();

        // then 5 tasks were returned in total, not per topic
        assertEquals(5, tasks.size());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchSuspendedTask() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when suspending the process instance
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        // then the external task cannot be fetched
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        assertEquals(0, externalTasks.size());

        // when activating the process instance
        runtimeService.activateProcessInstanceById(processInstance.getId());

        // then the task can be fetched
        externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();

        assertEquals(1, externalTasks.size());
    }

    /**
     * Note: this does not test a hard API guarantee, i.e. the test is stricter than the API (Javadoc).
     * Its purpose is to ensure that the API implementation is less error-prone to use.
     *
     * Bottom line: if there is good reason to change behavior such that this test breaks, it may
     * be ok to change the test.
     */
    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testFetchAndLockWithInitialBuilder() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        ExternalTaskQueryBuilder initialBuilder = externalTaskService.fetchAndLock(1, WORKER_ID);
        initialBuilder.topic(TOPIC_NAME, LOCK_TIME);

        // should execute regardless whether the initial builder is used or the builder returned by the
        // #topic invocation
        List<LockedExternalTask> tasks = initialBuilder.execute();

        // then
        assertEquals(1, tasks.size());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testComplete() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        externalTaskService.complete(externalTasks.get(0).getId(), WORKER_ID);

        // then
        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, externalTasks.size());

        ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId());
        assertThat(activityInstance)
                .hasStructure(describeActivityInstanceTree(processInstance.getProcessDefinitionId())
                        .activity("afterExternalTask").done());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testCompleteWithVariables() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        externalTaskService.complete(externalTasks.get(0).getId(), WORKER_ID,
                Variables.createVariables().putValue("var", 42));

        // then
        ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId());
        assertThat(activityInstance)
                .hasStructure(describeActivityInstanceTree(processInstance.getProcessDefinitionId())
                        .activity("afterExternalTask").done());

        assertEquals(42, runtimeService.getVariable(processInstance.getId(), "var"));
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testCompleteWithWrongWorkerId() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then it is not possible to complete the task with a different worker id
        try {
            externalTaskService.complete(externalTasks.get(0).getId(), "someCrazyWorkerId");
            fail("exception expected");
        } catch (BadUserRequestException e) {
            assertTextPresent("cannot be completed by worker 'someCrazyWorkerId'. It is locked by worker '"
                    + WORKER_ID + "'.", e.getMessage());
        }
    }

    public void testCompleteNonExistingTask() {
        try {
            externalTaskService.complete("nonExistingTaskId", WORKER_ID);
            fail("exception expected");
        } catch (NotFoundException e) {
            // not found exception lets client distinguish this from other failures
            assertTextPresent("Cannot find external task with id nonExistingTaskId", e.getMessage());
        }
    }

    public void testCompleteNullTaskId() {
        try {
            externalTaskService.complete(null, WORKER_ID);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("Cannot find external task with id " + null, e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testCompleteNullWorkerId() {
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        try {
            externalTaskService.complete(task.getId(), null);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("workerId is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testCompleteSuspendedTask() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        LockedExternalTask task = externalTasks.get(0);

        // when suspending the process instance
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        // then the external task cannot be completed
        try {
            externalTaskService.complete(task.getId(), WORKER_ID);
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("ExternalTask with id '" + task.getId() + "' is suspended", e.getMessage());
        }

        assertProcessNotEnded(processInstance.getId());

        // when activating the process instance again
        runtimeService.activateProcessInstanceById(processInstance.getId());

        // then the task can be completed
        externalTaskService.complete(task.getId(), WORKER_ID);

        assertProcessEnded(processInstance.getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testLocking() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then the task is locked
        assertEquals(1, externalTasks.size());

        LockedExternalTask task = externalTasks.get(0);
        AssertUtil.assertEqualsSecondPrecision(nowPlus(LOCK_TIME), task.getLockExpirationTime());

        // and cannot be retrieved by another query
        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, externalTasks.size());

        // unless the expiration time expires
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(1, externalTasks.size());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testCompleteLockExpiredTask() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // and the lock expires without the task being reclaimed
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        // then the task can successfully be completed
        externalTaskService.complete(externalTasks.get(0).getId(), WORKER_ID);

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, externalTasks.size());
        assertProcessEnded(processInstance.getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testCompleteReclaimedLockExpiredTask() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // and the lock expires
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        // and it is reclaimed by another worker
        List<LockedExternalTask> reclaimedTasks = externalTaskService.fetchAndLock(1, "anotherWorkerId")
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then the first worker cannot complete the task
        try {
            externalTaskService.complete(externalTasks.get(0).getId(), WORKER_ID);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent(
                    "cannot be completed by worker '" + WORKER_ID + "'. It is locked by worker 'anotherWorkerId'.",
                    e.getMessage());
        }

        // and the second worker can
        externalTaskService.complete(reclaimedTasks.get(0).getId(), "anotherWorkerId");

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, externalTasks.size());
        assertProcessEnded(processInstance.getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testDeleteProcessInstance() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        runtimeService.deleteProcessInstance(processInstance.getId(), null);

        // then
        assertEquals(0,
                externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute().size());
        assertProcessEnded(processInstance.getId());
    }

    @Deployment
    public void testExternalTaskExecutionTreeExpansion() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("boundaryExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();
        LockedExternalTask externalTask = tasks.get(0);

        // when a non-interrupting boundary event is triggered meanwhile
        // such that the execution tree is expanded
        runtimeService.correlateMessage("Message");

        // then the external task can still be completed
        externalTaskService.complete(externalTask.getId(), WORKER_ID);

        ActivityInstance activityInstance = runtimeService.getActivityInstance(processInstance.getId());
        assertThat(activityInstance)
                .hasStructure(describeActivityInstanceTree(processInstance.getProcessDefinitionId())
                        .activity("afterBoundaryTask").done());

        Task afterBoundaryTask = taskService.createTaskQuery().singleResult();
        taskService.complete(afterBoundaryTask.getId());

        assertProcessEnded(processInstance.getId());
    }

    @Deployment
    public void testExternalTaskExecutionTreeCompaction() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("concurrentExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();
        LockedExternalTask externalTask = tasks.get(0);

        Task userTask = taskService.createTaskQuery().singleResult();

        // when the user task completes meanwhile, thereby trigger execution tree compaction
        taskService.complete(userTask.getId());

        // then the external task can still be completed
        externalTaskService.complete(externalTask.getId(), WORKER_ID);

        tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, tasks.size());

        assertProcessEnded(processInstance.getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testUnlock() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        LockedExternalTask task = externalTasks.get(0);

        // when unlocking the task
        externalTaskService.unlock(task.getId());

        // then it can be acquired again
        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();

        assertEquals(1, externalTasks.size());
        LockedExternalTask reAcquiredTask = externalTasks.get(0);
        assertEquals(task.getId(), reAcquiredTask.getId());
    }

    public void testUnlockNullTaskId() {
        try {
            externalTaskService.unlock(null);
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("externalTaskId is null", e.getMessage());
        }
    }

    public void testUnlockNonExistingTask() {
        try {
            externalTaskService.unlock("nonExistingId");
            fail("expected exception");
        } catch (NotFoundException e) {
            // not found exception lets client distinguish this from other failures
            assertTextPresent("Cannot find external task with id nonExistingId", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailure() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        // when submitting a failure (after a simulated processing time of three seconds)
        ClockUtil.setCurrentTime(nowPlus(3000L));

        String errorMessage = "errorMessage";
        externalTaskService.handleFailure(task.getId(), WORKER_ID, errorMessage, 5, 3000L);

        // then the task cannot be immediately acquired again
        tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, tasks.size());

        // and no incident exists because there are still retries left
        assertEquals(0, runtimeService.createIncidentQuery().count());

        // but when the retry time expires, the task is available again
        ClockUtil.setCurrentTime(nowPlus(4000L));

        tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(1, tasks.size());

        // and the retries and error message are accessible
        task = tasks.get(0);
        assertEquals(errorMessage, task.getErrorMessage());
        assertEquals(5, (int) task.getRetries());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureWithErrorDetails() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        // when submitting a failure (after a simulated processing time of three seconds)
        ClockUtil.setCurrentTime(nowPlus(3000L));

        String errorMessage;
        String exceptionStackTrace;
        try {
            RuntimeSqlException cause = new RuntimeSqlException("test cause");
            for (int i = 0; i < 10; i++) {
                cause = new RuntimeSqlException(cause);
            }
            throw cause;
        } catch (RuntimeException e) {
            exceptionStackTrace = ExceptionUtils.getStackTrace(e);
            errorMessage = e.getMessage();
            while (errorMessage.length() < 1000) {
                errorMessage = errorMessage + ":" + e.getMessage();
            }
        }
        Assert.assertThat(exceptionStackTrace, is(notNullValue()));
        //  make sure that stack trace is longer then errorMessage DB field length
        Assert.assertThat(exceptionStackTrace.length(), is(greaterThan(4000)));
        externalTaskService.handleFailure(task.getId(), WORKER_ID, errorMessage, exceptionStackTrace, 5, 3000L);
        ClockUtil.setCurrentTime(nowPlus(4000L));
        tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        Assert.assertThat(tasks.size(), is(1));

        // verify that exception is accessible properly
        task = tasks.get(0);
        Assert.assertThat(task.getErrorMessage(), is(errorMessage.substring(0, 666)));
        Assert.assertThat(task.getRetries(), is(5));
        Assert.assertThat(externalTaskService.getExternalTaskErrorDetails(task.getId()), is(exceptionStackTrace));
        Assert.assertThat(task.getErrorDetails(), is(exceptionStackTrace));
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureZeroRetries() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        // when reporting a failure and setting retries to 0
        ClockUtil.setCurrentTime(nowPlus(3000L));

        String errorMessage = "errorMessage";
        externalTaskService.handleFailure(task.getId(), WORKER_ID, errorMessage, 0, 3000L);

        // then the task cannot be fetched anymore even when the lock expires
        ClockUtil.setCurrentTime(nowPlus(4000L));

        tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, tasks.size());

        // and an incident has been created
        Incident incident = runtimeService.createIncidentQuery().singleResult();

        assertNotNull(incident);
        assertNotNull(incident.getId());
        assertEquals(errorMessage, incident.getIncidentMessage());
        assertEquals(task.getExecutionId(), incident.getExecutionId());
        assertEquals("externalTask", incident.getActivityId());
        assertEquals(incident.getId(), incident.getCauseIncidentId());
        assertEquals("failedExternalTask", incident.getIncidentType());
        assertEquals(task.getProcessDefinitionId(), incident.getProcessDefinitionId());
        assertEquals(task.getProcessInstanceId(), incident.getProcessInstanceId());
        assertEquals(incident.getId(), incident.getRootCauseIncidentId());
        AssertUtil.assertEqualsSecondPrecision(nowMinus(4000L), incident.getIncidentTimestamp());
        assertEquals(task.getId(), incident.getConfiguration());
        assertNull(incident.getJobDefinitionId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureAndDeleteProcessInstance() {
        // given a failed external task with incident
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        externalTaskService.handleFailure(task.getId(), WORKER_ID, "someError", 0, LOCK_TIME);

        // when
        runtimeService.deleteProcessInstance(processInstance.getId(), null);

        // then
        assertProcessEnded(processInstance.getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureThenComplete() {
        // given a failed external task with incident
        runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        externalTaskService.handleFailure(task.getId(), WORKER_ID, "someError", 0, LOCK_TIME);

        // when
        externalTaskService.complete(task.getId(), WORKER_ID);

        // then the task has been completed nonetheless
        Task followingTask = taskService.createTaskQuery().singleResult();
        assertNotNull(followingTask);
        assertEquals("afterExternalTask", followingTask.getTaskDefinitionKey());

    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureWithWrongWorkerId() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then it is not possible to complete the task with a different worker id
        try {
            externalTaskService.handleFailure(externalTasks.get(0).getId(), "someCrazyWorkerId", "error", 5,
                    LOCK_TIME);
            fail("exception expected");
        } catch (BadUserRequestException e) {
            assertTextPresent("Failure of External Task " + externalTasks.get(0).getId()
                    + " cannot be reported by worker 'someCrazyWorkerId'. It is locked by worker '" + WORKER_ID
                    + "'.", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureNonExistingTask() {
        try {
            externalTaskService.handleFailure("nonExistingTaskId", WORKER_ID, "error", 5, LOCK_TIME);
            fail("exception expected");
        } catch (NotFoundException e) {
            // not found exception lets client distinguish this from other failures
            assertTextPresent("Cannot find external task with id nonExistingTaskId", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureNullTaskId() {
        try {
            externalTaskService.handleFailure(null, WORKER_ID, "error", 5, LOCK_TIME);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("Cannot find external task with id " + null, e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureNullWorkerId() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        try {
            externalTaskService.handleFailure(externalTasks.get(0).getId(), null, "error", 5, LOCK_TIME);
            fail("exception expected");
        } catch (NullValueException e) {
            assertTextPresent("workerId is null", e.getMessage());
        }

    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureNegativeLockDuration() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        try {
            externalTaskService.handleFailure(externalTasks.get(0).getId(), WORKER_ID, "error", 5, -LOCK_TIME);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("retryDuration is not greater than or equal to 0", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureNegativeRetries() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then
        try {
            externalTaskService.handleFailure(externalTasks.get(0).getId(), WORKER_ID, "error", -5, LOCK_TIME);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("retries is not greater than or equal to 0", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureNullErrorMessage() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // when
        externalTaskService.handleFailure(externalTasks.get(0).getId(), WORKER_ID, null, 5, LOCK_TIME);

        // then the failure was reported successfully and the error message is null
        ExternalTask task = externalTaskService.createExternalTaskQuery().singleResult();

        assertEquals(5, (int) task.getRetries());
        assertNull(task.getErrorMessage());
        assertNull(externalTaskService.getExternalTaskErrorDetails(task.getId()));
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleFailureSuspendedTask() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        LockedExternalTask task = externalTasks.get(0);

        // when suspending the process instance
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        // then a failure cannot be reported
        try {
            externalTaskService.handleFailure(externalTasks.get(0).getId(), WORKER_ID, "error", 5, LOCK_TIME);
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("ExternalTask with id '" + task.getId() + "' is suspended", e.getMessage());
        }

        assertProcessNotEnded(processInstance.getId());

        // when activating the process instance again
        runtimeService.activateProcessInstanceById(processInstance.getId());

        // then the failure can be reported successfully
        externalTaskService.handleFailure(externalTasks.get(0).getId(), WORKER_ID, "error", 5, LOCK_TIME);

        ExternalTask updatedTask = externalTaskService.createExternalTaskQuery().singleResult();
        assertEquals(5, (int) updatedTask.getRetries());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testSetRetries() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // when
        externalTaskService.setRetries(externalTasks.get(0).getId(), 5);

        // then
        ExternalTask task = externalTaskService.createExternalTaskQuery().singleResult();

        assertEquals(5, (int) task.getRetries());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testSetRetriesResolvesFailureIncident() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        LockedExternalTask lockedTask = externalTasks.get(0);
        externalTaskService.handleFailure(lockedTask.getId(), WORKER_ID, "error", 0, LOCK_TIME);

        Incident incident = runtimeService.createIncidentQuery().singleResult();

        // when
        externalTaskService.setRetries(lockedTask.getId(), 5);

        // then the incident is resolved
        assertEquals(0, runtimeService.createIncidentQuery().count());

        if (processEngineConfiguration.getHistoryLevel().getId() >= HistoryLevel.HISTORY_LEVEL_FULL.getId()) {

            HistoricIncident historicIncident = historyService.createHistoricIncidentQuery().singleResult();
            assertNotNull(historicIncident);
            assertEquals(incident.getId(), historicIncident.getId());
            assertTrue(historicIncident.isResolved());
        }

        // and the task can be fetched again
        ClockUtil.setCurrentTime(nowPlus(LOCK_TIME + 3000L));

        externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();

        assertEquals(1, externalTasks.size());
        assertEquals(lockedTask.getId(), externalTasks.get(0).getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testSetRetriesToZero() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        LockedExternalTask lockedTask = externalTasks.get(0);

        // when
        externalTaskService.setRetries(lockedTask.getId(), 0);

        // then
        Incident incident = runtimeService.createIncidentQuery().singleResult();
        assertNotNull(incident);
        assertEquals(lockedTask.getId(), incident.getConfiguration());

        // and resetting the retries removes the incident again
        externalTaskService.setRetries(lockedTask.getId(), 5);

        assertEquals(0, runtimeService.createIncidentQuery().count());

    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testSetRetriesNegative() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        try {
            // when
            externalTaskService.setRetries(externalTasks.get(0).getId(), -5);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("retries is not greater than or equal to 0", e.getMessage());
        }
    }

    public void testSetRetriesNonExistingTask() {
        try {
            externalTaskService.setRetries("someExternalTaskId", 5);
            fail("expected exception");
        } catch (NotFoundException e) {
            // not found exception lets client distinguish this from other failures
            assertTextPresent("externalTask is null", e.getMessage());
        }
    }

    public void testSetRetriesNullTaskId() {
        try {
            externalTaskService.setRetries(null, 5);
            fail("expected exception");
        } catch (NullValueException e) {
            assertTextPresent("externalTaskId is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testSetPriority() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // when
        externalTaskService.setPriority(externalTasks.get(0).getId(), 5);

        // then
        ExternalTask task = externalTaskService.createExternalTaskQuery().singleResult();

        assertEquals(5, (int) task.getPriority());
    }

    public void testSetPriorityNonExistingTask() {
        try {
            externalTaskService.setPriority("someExternalTaskId", 5);
            fail("expected exception");
        } catch (NotFoundException e) {
            // not found exception lets client distinguish this from other failures
            assertTextPresent("externalTask is null", e.getMessage());
        }
    }

    public void testSetPriorityNullTaskId() {
        try {
            externalTaskService.setPriority(null, 5);
            fail("expected exception");
        } catch (NullValueException e) {
            assertTextPresent("externalTaskId is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskWithPriorityProcess.bpmn20.xml")
    public void testAfterSetPriorityFetchHigherTask() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskWithPriorityProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(2, WORKER_ID, true)
                .topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(2, externalTasks.size());
        LockedExternalTask task = externalTasks.get(1);
        assertEquals(0, task.getPriority());
        externalTaskService.setPriority(task.getId(), 9);
        // and the lock expires without the task being reclaimed
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        // then
        externalTasks = externalTaskService.fetchAndLock(1, "anotherWorkerId", true).topic(TOPIC_NAME, LOCK_TIME)
                .execute();
        assertEquals(externalTasks.get(0).getPriority(), 9);
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testSetPriorityLockExpiredTask() {
        // given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // and the lock expires without the task being reclaimed
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        // then the priority can be set
        externalTaskService.setPriority(externalTasks.get(0).getId(), 9);

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID, true).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(1, externalTasks.size());
        assertEquals(externalTasks.get(0).getPriority(), 9);
    }

    @Deployment
    public void testCancelExternalTaskWithBoundaryEvent() {
        // given
        runtimeService.startProcessInstanceByKey("boundaryExternalTaskProcess");
        assertEquals(1, externalTaskService.createExternalTaskQuery().count());

        // when the external task is cancelled by a boundary event
        runtimeService.correlateMessage("Message");

        // then the external task instance has been removed
        assertEquals(0, externalTaskService.createExternalTaskQuery().count());

        Task afterBoundaryTask = taskService.createTaskQuery().singleResult();
        assertNotNull(afterBoundaryTask);
        assertEquals("afterBoundaryTask", afterBoundaryTask.getTaskDefinitionKey());

    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnError() {
        //given
        runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");
        // when
        List<LockedExternalTask> externalTasks = helperHandleBpmnError(1, WORKER_ID, TOPIC_NAME, LOCK_TIME,
                "ERROR-OCCURED");
        //then
        assertEquals(0, externalTasks.size());
        assertEquals(0, externalTaskService.createExternalTaskQuery().count());
        Task afterBpmnError = taskService.createTaskQuery().singleResult();
        assertNotNull(afterBpmnError);
        assertEquals(afterBpmnError.getTaskDefinitionKey(), "afterBpmnError");
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnErrorWithoutDefinedBoundary() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        //when
        List<LockedExternalTask> externalTasks = helperHandleBpmnError(1, WORKER_ID, TOPIC_NAME, LOCK_TIME,
                "ERROR-OCCURED");

        //then
        assertEquals(0, externalTasks.size());
        assertEquals(0, externalTaskService.createExternalTaskQuery().count());
        Task afterBpmnError = taskService.createTaskQuery().singleResult();
        assertNull(afterBpmnError);
        assertProcessEnded(processInstance.getId());
    }

    /**
     * Helper method to handle a bmpn error on an external task, which is fetched with the given parameters.
     *
     * @param taskCount the count of task to fetch
     * @param workerID the worker id
     * @param topicName the topic name of the external task
     * @param lockTime the lock time for the fetch
     * @param errorCode the error code of the bpmn error
     * @return returns the locked external tasks after the bpmn error was handled
     */
    public List<LockedExternalTask> helperHandleBpmnError(int taskCount, String workerID, String topicName,
            long lockTime, String errorCode) {
        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(taskCount, workerID)
                .topic(topicName, lockTime).execute();

        externalTaskService.handleBpmnError(externalTasks.get(0).getId(), workerID, errorCode);

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        return externalTasks;
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnErrorLockExpiredTask() {
        //given
        runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");
        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // and the lock expires without the task being reclaimed
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        externalTaskService.handleBpmnError(externalTasks.get(0).getId(), WORKER_ID, "ERROR-OCCURED");

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();

        assertEquals(0, externalTasks.size());
        assertEquals(0, externalTaskService.createExternalTaskQuery().count());
        Task afterBpmnError = taskService.createTaskQuery().singleResult();
        assertNotNull(afterBpmnError);
        assertEquals(afterBpmnError.getTaskDefinitionKey(), "afterBpmnError");
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnErrorReclaimedLockExpiredTaskWithoutDefinedBoundary() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        handleBpmnErrorReclaimedLockExpiredTask();
        assertProcessEnded(processInstance.getId());
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/twoExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnErrorReclaimedLockExpiredTaskWithBoundary() {
        // given
        runtimeService.startProcessInstanceByKey("twoExternalTaskProcess");
        //then
        handleBpmnErrorReclaimedLockExpiredTask();
    }

    /**
     * Helpher method which reclaims an external task after the lock is expired.
     */
    public void handleBpmnErrorReclaimedLockExpiredTask() {
        // when
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // and the lock expires
        ClockUtil.setCurrentTime(new DateTime(ClockUtil.getCurrentTime()).plus(LOCK_TIME * 2).toDate());

        // and it is reclaimed by another worker
        List<LockedExternalTask> reclaimedTasks = externalTaskService.fetchAndLock(1, "anotherWorkerId")
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        // then the first worker cannot complete the task
        try {
            externalTaskService.handleBpmnError(externalTasks.get(0).getId(), WORKER_ID, "ERROR-OCCURED");
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("Bpmn error of External Task " + externalTasks.get(0).getId()
                    + " cannot be reported by worker '" + WORKER_ID
                    + "'. It is locked by worker 'anotherWorkerId'.", e.getMessage());
        }

        // and the second worker can
        externalTaskService.complete(reclaimedTasks.get(0).getId(), "anotherWorkerId");

        externalTasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME).execute();
        assertEquals(0, externalTasks.size());
    }

    public void testHandleBpmnErrorNonExistingTask() {
        try {
            externalTaskService.handleBpmnError("nonExistingTaskId", WORKER_ID, "ERROR-OCCURED");
            fail("exception expected");
        } catch (NotFoundException e) {
            // not found exception lets client distinguish this from other failures
            assertTextPresent("Cannot find external task with id nonExistingTaskId", e.getMessage());
        }
    }

    public void testHandleBpmnNullTaskId() {
        try {
            externalTaskService.handleBpmnError(null, WORKER_ID, "ERROR-OCCURED");
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("Cannot find external task with id " + null, e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnNullErrorCode() {
        //given
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        //when
        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        //then
        LockedExternalTask task = tasks.get(0);
        try {
            externalTaskService.handleBpmnError(task.getId(), WORKER_ID, null);
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("errorCode is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnErrorNullWorkerId() {
        runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");

        List<LockedExternalTask> tasks = externalTaskService.fetchAndLock(1, WORKER_ID).topic(TOPIC_NAME, LOCK_TIME)
                .execute();

        LockedExternalTask task = tasks.get(0);

        try {
            externalTaskService.handleBpmnError(task.getId(), null, "ERROR-OCCURED");
            fail("exception expected");
        } catch (ProcessEngineException e) {
            assertTextPresent("workerId is null", e.getMessage());
        }
    }

    @Deployment(resources = "org/camunda/bpm/engine/test/api/externaltask/oneExternalTaskProcess.bpmn20.xml")
    public void testHandleBpmnErrorSuspendedTask() {
        // given
        ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("oneExternalTaskProcess");
        List<LockedExternalTask> externalTasks = externalTaskService.fetchAndLock(5, WORKER_ID)
                .topic(TOPIC_NAME, LOCK_TIME).execute();

        LockedExternalTask task = externalTasks.get(0);

        // when suspending the process instance
        runtimeService.suspendProcessInstanceById(processInstance.getId());

        // then the external task cannot be completed
        try {
            externalTaskService.handleBpmnError(task.getId(), WORKER_ID, "ERROR-OCCURED");
            fail("expected exception");
        } catch (ProcessEngineException e) {
            assertTextPresent("ExternalTask with id '" + task.getId() + "' is suspended", e.getMessage());
        }
    }

    protected Date nowPlus(long millis) {
        return new Date(ClockUtil.getCurrentTime().getTime() + millis);
    }

    protected Date nowMinus(long millis) {
        return new Date(ClockUtil.getCurrentTime().getTime() - millis);
    }
}