TSK-1915: Add requestReview functionality
This commit is contained in:
parent
d32a6189c7
commit
ef875dd42a
|
@ -0,0 +1,197 @@
|
|||
package acceptance.task.requestreview;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.catchThrowableOfType;
|
||||
import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification;
|
||||
import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.stream.Stream;
|
||||
import org.assertj.core.api.ThrowableAssert.ThrowingCallable;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.DynamicTest;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.TestFactory;
|
||||
import org.junit.jupiter.api.function.ThrowingConsumer;
|
||||
|
||||
import pro.taskana.classification.api.ClassificationService;
|
||||
import pro.taskana.classification.api.models.ClassificationSummary;
|
||||
import pro.taskana.common.internal.util.EnumUtil;
|
||||
import pro.taskana.task.api.TaskService;
|
||||
import pro.taskana.task.api.TaskState;
|
||||
import pro.taskana.task.api.exceptions.InvalidOwnerException;
|
||||
import pro.taskana.task.api.exceptions.InvalidTaskStateException;
|
||||
import pro.taskana.task.api.models.ObjectReference;
|
||||
import pro.taskana.task.api.models.Task;
|
||||
import pro.taskana.testapi.DefaultTestEntities;
|
||||
import pro.taskana.testapi.TaskanaInject;
|
||||
import pro.taskana.testapi.TaskanaIntegrationTest;
|
||||
import pro.taskana.testapi.builder.TaskBuilder;
|
||||
import pro.taskana.testapi.builder.WorkbasketAccessItemBuilder;
|
||||
import pro.taskana.testapi.security.WithAccessId;
|
||||
import pro.taskana.workbasket.api.WorkbasketPermission;
|
||||
import pro.taskana.workbasket.api.WorkbasketService;
|
||||
import pro.taskana.workbasket.api.exceptions.MismatchedWorkbasketPermissionException;
|
||||
import pro.taskana.workbasket.api.models.WorkbasketSummary;
|
||||
|
||||
@TaskanaIntegrationTest
|
||||
class RequestReviewAccTest {
|
||||
@TaskanaInject TaskService taskService;
|
||||
|
||||
ClassificationSummary defaultClassificationSummary;
|
||||
WorkbasketSummary defaultWorkbasketSummary;
|
||||
ObjectReference defaultObjectReference;
|
||||
|
||||
@WithAccessId(user = "businessadmin")
|
||||
@BeforeAll
|
||||
void setup(ClassificationService classificationService, WorkbasketService workbasketService)
|
||||
throws Exception {
|
||||
defaultClassificationSummary =
|
||||
defaultTestClassification().buildAndStoreAsSummary(classificationService);
|
||||
defaultWorkbasketSummary = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService);
|
||||
|
||||
WorkbasketAccessItemBuilder.newWorkbasketAccessItem()
|
||||
.workbasketId(defaultWorkbasketSummary.getId())
|
||||
.accessId("user-1-1")
|
||||
.permission(WorkbasketPermission.READ)
|
||||
.permission(WorkbasketPermission.APPEND)
|
||||
.buildAndStore(workbasketService);
|
||||
|
||||
defaultObjectReference = DefaultTestEntities.defaultTestObjectReference().build();
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@Test
|
||||
void should_RequestReview_WhenTaskIsClaimed() throws Exception {
|
||||
Instant now = Instant.now();
|
||||
Task task = createTaskClaimedByUser("user-1-1").buildAndStore(taskService);
|
||||
|
||||
Task result = taskService.requestReview(task.getId());
|
||||
|
||||
assertThat(result.getState()).isEqualTo(TaskState.READY_FOR_REVIEW);
|
||||
assertThat(result.getOwner()).isNull();
|
||||
assertThat(result.getModified()).isAfterOrEqualTo(now);
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@Test
|
||||
void should_ForceRequestReview_WhenTaskIsClaimedByDifferentUser() throws Exception {
|
||||
Instant now = Instant.now();
|
||||
Task task = createTaskClaimedByUser("user-1-2").buildAndStore(taskService);
|
||||
|
||||
Task result = taskService.forceRequestReview(task.getId());
|
||||
|
||||
assertThat(result.getState()).isEqualTo(TaskState.READY_FOR_REVIEW);
|
||||
assertThat(result.getOwner()).isNull();
|
||||
assertThat(result.getModified()).isAfterOrEqualTo(now);
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@Test
|
||||
void should_ForceRequestReview_WhenTaskIsInReviewByDifferentUser() throws Exception {
|
||||
Instant now = Instant.now();
|
||||
Task task =
|
||||
createTaskClaimedByUser("user-1-2").state(TaskState.IN_REVIEW).buildAndStore(taskService);
|
||||
|
||||
Task result = taskService.forceRequestReview(task.getId());
|
||||
|
||||
assertThat(result.getState()).isEqualTo(TaskState.READY_FOR_REVIEW);
|
||||
assertThat(result.getOwner()).isNull();
|
||||
assertThat(result.getModified()).isAfterOrEqualTo(now);
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@TestFactory
|
||||
Stream<DynamicTest> should_ForceRequestReview_WhenTaskIsNotInEndState() {
|
||||
List<TaskState> testCases = Arrays.asList(EnumUtil.allValuesExceptFor(TaskState.END_STATES));
|
||||
ThrowingConsumer<TaskState> test =
|
||||
state -> {
|
||||
Instant now = Instant.now();
|
||||
Task task = createDefaultTask().state(state).buildAndStore(taskService);
|
||||
Task result = taskService.forceRequestReview(task.getId());
|
||||
|
||||
assertThat(result.getState()).isEqualTo(TaskState.READY_FOR_REVIEW);
|
||||
assertThat(result.getOwner()).isNull();
|
||||
assertThat(result.getModified()).isAfterOrEqualTo(now);
|
||||
};
|
||||
return DynamicTest.stream(testCases.iterator(), TaskState::name, test);
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@Test
|
||||
void should_ThrowException_When_RequestingReviewAndTaskIsClaimedByDifferentOwner()
|
||||
throws Exception {
|
||||
Task task = createTaskClaimedByUser("user-1-2").buildAndStore(taskService);
|
||||
|
||||
ThrowingCallable call = () -> taskService.requestReview(task.getId());
|
||||
|
||||
InvalidOwnerException e = catchThrowableOfType(call, InvalidOwnerException.class);
|
||||
assertThat(e.getCurrentUserId()).isEqualTo("user-1-1");
|
||||
assertThat(e.getTaskId()).isEqualTo(task.getId());
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@TestFactory
|
||||
Stream<DynamicTest> should_ThrowException_When_RequestingReviewAndTaskIsNotClaimed() {
|
||||
List<TaskState> invalidStates = Arrays.asList(EnumUtil.allValuesExceptFor(TaskState.CLAIMED));
|
||||
|
||||
ThrowingConsumer<TaskState> test =
|
||||
state -> {
|
||||
Task task = createDefaultTask().state(state).buildAndStore(taskService);
|
||||
ThrowingCallable call = () -> taskService.requestReview(task.getId());
|
||||
|
||||
InvalidTaskStateException e = catchThrowableOfType(call, InvalidTaskStateException.class);
|
||||
assertThat(e.getRequiredTaskStates()).containsExactly(TaskState.CLAIMED);
|
||||
assertThat(e.getTaskState()).isEqualTo(state);
|
||||
assertThat(e.getTaskId()).isEqualTo(task.getId());
|
||||
};
|
||||
return DynamicTest.stream(invalidStates.iterator(), TaskState::name, test);
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-2")
|
||||
@Test
|
||||
void should_ThrowException_When_UserHasNoWorkbasketPermission() throws Exception {
|
||||
Task task = createTaskClaimedByUser("user-1-1").buildAndStore(taskService, "user-1-1");
|
||||
ThrowingCallable call = () -> taskService.requestReview(task.getId());
|
||||
|
||||
MismatchedWorkbasketPermissionException e =
|
||||
catchThrowableOfType(call, MismatchedWorkbasketPermissionException.class);
|
||||
assertThat(e.getRequiredPermissions()).containsExactly(WorkbasketPermission.READ);
|
||||
assertThat(e.getCurrentUserId()).isEqualTo("user-1-2");
|
||||
assertThat(e.getWorkbasketId()).isEqualTo(defaultWorkbasketSummary.getId());
|
||||
assertThat(e.getDomain()).isNull();
|
||||
assertThat(e.getWorkbasketKey()).isNull();
|
||||
}
|
||||
|
||||
@WithAccessId(user = "user-1-1")
|
||||
@TestFactory
|
||||
Stream<DynamicTest> should_ThrowException_When_ForceRequestReviewAndTaskIsInEndState() {
|
||||
List<TaskState> endStates = Arrays.asList(TaskState.END_STATES);
|
||||
|
||||
ThrowingConsumer<TaskState> test =
|
||||
state -> {
|
||||
Task task = createDefaultTask().state(state).buildAndStore(taskService);
|
||||
ThrowingCallable call = () -> taskService.forceRequestReview(task.getId());
|
||||
|
||||
InvalidTaskStateException e = catchThrowableOfType(call, InvalidTaskStateException.class);
|
||||
assertThat(e.getRequiredTaskStates())
|
||||
.containsExactlyInAnyOrder(EnumUtil.allValuesExceptFor(TaskState.END_STATES));
|
||||
assertThat(e.getTaskState()).isEqualTo(state);
|
||||
assertThat(e.getTaskId()).isEqualTo(task.getId());
|
||||
};
|
||||
return DynamicTest.stream(endStates.iterator(), TaskState::name, test);
|
||||
}
|
||||
|
||||
private TaskBuilder createTaskClaimedByUser(String owner) {
|
||||
return createDefaultTask().owner(owner).state(TaskState.CLAIMED);
|
||||
}
|
||||
|
||||
private TaskBuilder createDefaultTask() {
|
||||
return TaskBuilder.newTask()
|
||||
.classificationSummary(defaultClassificationSummary)
|
||||
.workbasketSummary(defaultWorkbasketSummary)
|
||||
.primaryObjRef(defaultObjectReference);
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@ public enum TaskHistoryEventType {
|
|||
UPDATED("UPDATED"),
|
||||
CLAIMED("CLAIMED"),
|
||||
CLAIM_CANCELLED("CLAIM_CANCELLED"),
|
||||
REQUESTED_REVIEW("REQUESTED_REVIEW"),
|
||||
COMPLETED("COMPLETED"),
|
||||
CANCELLED("CANCELLED"),
|
||||
TERMINATED("TERMINATED"),
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
package pro.taskana.spi.history.api.events.task;
|
||||
|
||||
import pro.taskana.task.api.models.Task;
|
||||
|
||||
/** The TaskRequestReviewEvent is fired after a review on a {@linkplain Task} has been requested. */
|
||||
public class TaskRequestReviewEvent extends TaskHistoryEvent {
|
||||
|
||||
public TaskRequestReviewEvent(String id, Task task, String userId, String details) {
|
||||
super(id, task, userId, details);
|
||||
eventType = (TaskHistoryEventType.REQUESTED_REVIEW.getName());
|
||||
created = task.getModified();
|
||||
}
|
||||
}
|
|
@ -16,6 +16,7 @@ import pro.taskana.spi.routing.api.TaskRoutingProvider;
|
|||
import pro.taskana.task.api.exceptions.AttachmentPersistenceException;
|
||||
import pro.taskana.task.api.exceptions.InvalidOwnerException;
|
||||
import pro.taskana.task.api.exceptions.InvalidStateException;
|
||||
import pro.taskana.task.api.exceptions.InvalidTaskStateException;
|
||||
import pro.taskana.task.api.exceptions.ObjectReferencePersistenceException;
|
||||
import pro.taskana.task.api.exceptions.TaskAlreadyExistException;
|
||||
import pro.taskana.task.api.exceptions.TaskCommentNotFoundException;
|
||||
|
@ -99,6 +100,39 @@ public interface TaskService {
|
|||
throws TaskNotFoundException, InvalidStateException, InvalidOwnerException,
|
||||
NotAuthorizedException;
|
||||
|
||||
/**
|
||||
* Request review for an existing {@linkplain Task} that is in {@linkplain TaskState#CLAIMED}.
|
||||
*
|
||||
* @param taskId the {@linkplain Task#getId() id} of the specified {@linkplain Task}
|
||||
* @return the {@linkplain Task} after a review has been requested
|
||||
* @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain
|
||||
* Task} with taskId is not in {@linkplain TaskState#CLAIMED}
|
||||
* @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found
|
||||
* @throws InvalidOwnerException if the {@linkplain Task} is claimed by another user
|
||||
* @throws NotAuthorizedException if the current user has no {@linkplain
|
||||
* WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in
|
||||
*/
|
||||
Task requestReview(String taskId)
|
||||
throws InvalidTaskStateException, TaskNotFoundException, NotAuthorizedException,
|
||||
InvalidOwnerException;
|
||||
|
||||
/**
|
||||
* Request review for an existing {@linkplain Task} even if the current user is not the
|
||||
* {@linkplain Task#getOwner() owner} or the Task is not in {@linkplain TaskState#CLAIMED} yet.
|
||||
*
|
||||
* @param taskId the {@linkplain Task#getId() id} of the specified {@linkplain Task}
|
||||
* @return the {@linkplain Task} after a review has been requested
|
||||
* @throws InvalidTaskStateException if the {@linkplain Task#getState() state} of the {@linkplain
|
||||
* Task} with taskId is one of the {@linkplain TaskState#END_STATES}
|
||||
* @throws TaskNotFoundException if the {@linkplain Task} with taskId wasn't found
|
||||
* @throws InvalidOwnerException cannot be thrown
|
||||
* @throws NotAuthorizedException if the current user has no {@linkplain
|
||||
* WorkbasketPermission#READ} for the {@linkplain Workbasket} the {@linkplain Task} is in
|
||||
*/
|
||||
Task forceRequestReview(String taskId)
|
||||
throws InvalidTaskStateException, TaskNotFoundException, NotAuthorizedException,
|
||||
InvalidOwnerException;
|
||||
|
||||
/**
|
||||
* Complete a claimed {@linkplain Task} as {@linkplain Task#getOwner() owner} or {@linkplain
|
||||
* TaskanaRole#ADMIN} and update {@linkplain Task#getState() state} and timestamps.
|
||||
|
|
|
@ -47,6 +47,7 @@ import pro.taskana.spi.history.api.events.task.TaskClaimCancelledEvent;
|
|||
import pro.taskana.spi.history.api.events.task.TaskClaimedEvent;
|
||||
import pro.taskana.spi.history.api.events.task.TaskCompletedEvent;
|
||||
import pro.taskana.spi.history.api.events.task.TaskCreatedEvent;
|
||||
import pro.taskana.spi.history.api.events.task.TaskRequestReviewEvent;
|
||||
import pro.taskana.spi.history.api.events.task.TaskTerminatedEvent;
|
||||
import pro.taskana.spi.history.api.events.task.TaskUpdatedEvent;
|
||||
import pro.taskana.spi.history.internal.HistoryEventManager;
|
||||
|
@ -167,6 +168,20 @@ public class TaskServiceImpl implements TaskService {
|
|||
return this.cancelClaim(taskId, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task requestReview(String taskId)
|
||||
throws InvalidTaskStateException, TaskNotFoundException, NotAuthorizedException,
|
||||
InvalidOwnerException {
|
||||
return requestReview(taskId, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task forceRequestReview(String taskId)
|
||||
throws InvalidTaskStateException, TaskNotFoundException, NotAuthorizedException,
|
||||
InvalidOwnerException {
|
||||
return requestReview(taskId, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Task completeTask(String taskId)
|
||||
throws TaskNotFoundException, InvalidOwnerException, InvalidStateException,
|
||||
|
@ -1163,9 +1178,7 @@ public class TaskServiceImpl implements TaskService {
|
|||
taskanaEngine.openConnection();
|
||||
task = (TaskImpl) getTask(taskId);
|
||||
|
||||
TaskImpl oldTask = task.copy();
|
||||
oldTask.setId(taskId);
|
||||
oldTask.setExternalId(task.getExternalId());
|
||||
TaskImpl oldTask = duplicateTaskExactly(task);
|
||||
Instant now = Instant.now();
|
||||
|
||||
checkPreconditionsForClaimTask(task, forceClaim);
|
||||
|
@ -1191,6 +1204,53 @@ public class TaskServiceImpl implements TaskService {
|
|||
return task;
|
||||
}
|
||||
|
||||
private Task requestReview(String taskId, boolean force)
|
||||
throws TaskNotFoundException, NotAuthorizedException, InvalidTaskStateException,
|
||||
InvalidOwnerException {
|
||||
String userId = taskanaEngine.getEngine().getCurrentUserContext().getUserid();
|
||||
TaskImpl task;
|
||||
try {
|
||||
taskanaEngine.openConnection();
|
||||
task = (TaskImpl) getTask(taskId);
|
||||
|
||||
TaskImpl oldTask = duplicateTaskExactly(task);
|
||||
|
||||
if (force && task.getState().isEndState()) {
|
||||
throw new InvalidTaskStateException(
|
||||
task.getId(), task.getState(), EnumUtil.allValuesExceptFor(TaskState.END_STATES));
|
||||
}
|
||||
if (!force && task.getState() != TaskState.CLAIMED) {
|
||||
throw new InvalidTaskStateException(task.getId(), task.getState(), TaskState.CLAIMED);
|
||||
}
|
||||
if (!force && !task.getOwner().equals(userId)) {
|
||||
throw new InvalidOwnerException(userId, task.getId());
|
||||
}
|
||||
|
||||
task.setState(TaskState.READY_FOR_REVIEW);
|
||||
task.setOwner(null);
|
||||
task.setModified(Instant.now());
|
||||
|
||||
taskMapper.update(task);
|
||||
if (LOGGER.isDebugEnabled()) {
|
||||
LOGGER.debug("Requested review for Task '{}' by user '{}'.", taskId, userId);
|
||||
}
|
||||
if (historyEventManager.isEnabled()) {
|
||||
String changeDetails =
|
||||
ObjectAttributeChangeDetector.determineChangesInAttributes(oldTask, task);
|
||||
|
||||
historyEventManager.createEvent(
|
||||
new TaskRequestReviewEvent(
|
||||
IdGenerator.generateWithPrefix(IdGenerator.ID_PREFIX_TASK_HISTORY_EVENT),
|
||||
task,
|
||||
taskanaEngine.getEngine().getCurrentUserContext().getUserid(),
|
||||
changeDetails));
|
||||
}
|
||||
} finally {
|
||||
taskanaEngine.returnConnection();
|
||||
}
|
||||
return task;
|
||||
}
|
||||
|
||||
private static void claimActionsOnTask(TaskSummaryImpl task, String userId, Instant now) {
|
||||
task.setOwner(userId);
|
||||
task.setModified(now);
|
||||
|
@ -1278,9 +1338,7 @@ public class TaskServiceImpl implements TaskService {
|
|||
taskanaEngine.openConnection();
|
||||
task = (TaskImpl) getTask(taskId);
|
||||
|
||||
TaskImpl oldTask = task.copy();
|
||||
oldTask.setId(taskId);
|
||||
oldTask.setExternalId(task.getExternalId());
|
||||
TaskImpl oldTask = duplicateTaskExactly(task);
|
||||
|
||||
TaskState state = task.getState();
|
||||
if (state.isEndState()) {
|
||||
|
@ -1904,4 +1962,13 @@ public class TaskServiceImpl implements TaskService {
|
|||
task,
|
||||
taskanaEngine.getEngine().getCurrentUserContext().getUserid())));
|
||||
}
|
||||
|
||||
private TaskImpl duplicateTaskExactly(TaskImpl task) {
|
||||
TaskImpl oldTask = task.copy();
|
||||
oldTask.setId(task.getId());
|
||||
oldTask.setExternalId(task.getExternalId());
|
||||
oldTask.setAttachments(task.getAttachments());
|
||||
oldTask.setSecondaryObjectReferences(task.getSecondaryObjectReferences());
|
||||
return oldTask;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue