TSK-1915: Add requestReview functionality

This commit is contained in:
ryzheboka 2022-07-19 17:46:30 +02:00 committed by Elena Mokeeva
parent d32a6189c7
commit ef875dd42a
5 changed files with 318 additions and 6 deletions

View File

@ -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);
}
}

View File

@ -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"),

View File

@ -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();
}
}

View File

@ -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.

View File

@ -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;
}
}