diff --git a/common/taskana-common-data/src/main/resources/sql/test-data/task.sql b/common/taskana-common-data/src/main/resources/sql/test-data/task.sql index d08f034f6f..d3d1dbdc64 100644 --- a/common/taskana-common-data/src/main/resources/sql/test-data/task.sql +++ b/common/taskana-common-data/src/main/resources/sql/test-data/task.sql @@ -29,6 +29,7 @@ INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000021', 'ETI:0000000 INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000022', 'ETI:000000000000000000000000000000000022', '2018-01-29 15:55:22', null , null , '2018-01-29 15:55:22', null , '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000001' , 'GPK_KSC' , 'DOMAIN_A', 'PI_0000000000022' , 'DOC_0000000000000000022' , null , '00' , 'PASystem' , '00' , 'SDNR' , '11223344' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000023', 'ETI:000000000000000000000000000000000023', '2018-01-29 15:55:23', null , null , '2018-01-29 15:55:23', null , '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000001' , 'GPK_KSC' , 'DOMAIN_A', 'PI_0000000000023' , 'DOC_0000000000000000023' , null , '00' , 'PASystem' , '00' , 'SDNR' , '11223344' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , 'lnp' , '' , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000024', 'ETI:000000000000000000000000000000000024', '2018-01-29 15:55:24', null , null , '2018-01-29 15:55:24', null , '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000001' , 'GPK_KSC' , 'DOMAIN_A', 'PI_0000000000024' , 'DOC_0000000000000000024' , null , '00' , 'PASystem' , '00' , 'SDNR' , '11223344' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , '' , null , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); +INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000201', 'ETI:000000000000000000000000000000000201', '2023-07-31 15:55:01', '2023-07-31 15:55:00', null , '2023-07-31 15:55:01', null , '2023-07-31 15:55:00', '2023-08-02 15:55:00', 'Task201' , 'creator_user_id' , 'Lorem ipsum was n Quatsch dolor sit amet.', 'Some custom Note' , 2 , -1 , 'READY' , 'EXTERN' , 'L110102' , 'CLI:100000000000000000000000000000000002', 'WBI:100000000000000000000000000000000006' , 'USER-1-1' , 'DOMAIN_A', 'BPI21' , 'PBPI21' , 'user-1-1' , 'MyCompany1', 'MySystem1', 'MyInstance1' , 'MyType1', 'MyValue1' , true , false , null , 'NONE' , null , 'pqr' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , '' , 'abc' , '' , '' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); -- TASK TABLE (ID , EXTERNAL_ID , CREATED , CLAIMED , COMPLETED , modified , received , planned , due , name , creator , description , note , priority, manual_priority, state , classification_category , classification_key, classification_id , workbasket_id , workbasket_key, domain , business_process_id, parent_business_process_id, owner , por_company , por_system , por_system_instance, por_type , por_value , is_read, is_transferred,callback_info , callback_state , custom_attributes ,custom1 ,custom2, ,custom3, ,custom4 ,custom5 ,custom6 ,custom7 ,custom8 ,custom9 ,custom10 ,custom11, ,custom12 ,custom13 ,custom14 ,custom15 ,custom16 , custom-int-1, custom-int-2, custom-int-3, custom-int-4, custom-int-5, custom-int-6, custom-int-7, custom-int-8 -- Tasks for WorkOnTaskAccTest INSERT INTO TASK VALUES('TKI:000000000000000000000000000000000025', 'ETI:000000000000000000000000000000000025', '2018-01-29 15:55:24', null , null , '2018-01-29 15:55:24', '2018-01-29 15:55:24', '2018-01-29 15:55:00', '2018-01-30 15:55:00', 'Widerruf' , 'creator_user_id' , 'Widerruf' , null , 2 , -1 , 'READY' , 'EXTERN' , 'L1050' , 'CLI:100000000000000000000000000000000003', 'WBI:100000000000000000000000000000000007' , 'USER-1-2' , 'DOMAIN_A', 'PI_0000000000025' , 'DOC_0000000000000000025' , null , 'abcd00' , 'PASystem' , '00' , 'SDNR' , '98765432' , false , false , null , 'NONE' , null , '' , '' , '' , '' , '' , '' , '' , '' , '' , 'ert' , 'ert' , 'ert' , 'ert' , 'abc' , 'ert' , 'ert' , 1 , 2 , 3 , 4 , 5 , 6 , 7 , 8 ); diff --git a/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingDayCalculatorImpl.java b/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingDayCalculatorImpl.java new file mode 100644 index 0000000000..ddaf46d8b2 --- /dev/null +++ b/common/taskana-common/src/main/java/pro/taskana/common/internal/workingtime/WorkingDayCalculatorImpl.java @@ -0,0 +1,118 @@ +package pro.taskana.common.internal.workingtime; + +import java.time.DayOfWeek; +import java.time.Duration; +import java.time.Instant; +import java.time.LocalDate; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.stream.LongStream; +import pro.taskana.common.api.WorkingTimeCalculator; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.api.exceptions.SystemException; + +public class WorkingDayCalculatorImpl implements WorkingTimeCalculator { + + private final ZoneId zoneId; + private final HolidaySchedule holidaySchedule; + + public WorkingDayCalculatorImpl(HolidaySchedule holidaySchedule, ZoneId zoneId) { + this.holidaySchedule = holidaySchedule; + this.zoneId = zoneId; + } + + @Override + public Instant subtractWorkingTime(Instant workStart, Duration workingTime) + throws InvalidArgumentException { + long days = convertWorkingDaysToDays(workStart, -workingTime.toDays(), ZeroDirection.SUB_DAYS); + return workStart.plus(Duration.ofDays(days)); + } + + @Override + public Instant addWorkingTime(Instant workStart, Duration workingTime) + throws InvalidArgumentException { + long days = convertWorkingDaysToDays(workStart, workingTime.toDays(), ZeroDirection.ADD_DAYS); + return workStart.plus(Duration.ofDays(days)); + } + + @Override + public Duration workingTimeBetween(Instant first, Instant second) + throws InvalidArgumentException { + long days = Duration.between(first, second).abs().toDays(); + Instant firstInstant = first.isBefore(second) ? first : second; + + long workingDaysBetween = + LongStream.range(1, days) + .mapToObj(day -> isWorkingDay(firstInstant.plus(day, ChronoUnit.DAYS))) + .filter(t -> t) + .count(); + return Duration.ofDays(workingDaysBetween); + } + + @Override + public boolean isWorkingDay(Instant instant) { + return !isWeekend(instant) && !isHoliday(instant); + } + + @Override + public boolean isWeekend(Instant instant) { + DayOfWeek dayOfWeek = toDayOfWeek(instant); + return dayOfWeek == DayOfWeek.SATURDAY || dayOfWeek == DayOfWeek.SUNDAY; + } + + @Override + public boolean isHoliday(Instant instant) { + return holidaySchedule.isHoliday(toLocalDate(instant)); + } + + @Override + public boolean isGermanHoliday(Instant instant) { + return holidaySchedule.isGermanHoliday(toLocalDate(instant)); + } + + private long convertWorkingDaysToDays( + final Instant startTime, long numberOfDays, ZeroDirection zeroDirection) { + if (startTime == null) { + throw new SystemException( + "Internal Error: convertWorkingDaysToDays was called with a null startTime"); + } + int direction = calculateDirection(numberOfDays, zeroDirection); + long limit = Math.abs(numberOfDays); + return LongStream.iterate(0, i -> i + direction) + .filter(day -> isWorkingDay(startTime.plus(day, ChronoUnit.DAYS))) + .skip(limit) + .findFirst() + .orElse(0); + } + + private int calculateDirection(long numberOfDays, ZeroDirection zeroDirection) { + if (numberOfDays == 0) { + return zeroDirection.getDirection(); + } else { + return numberOfDays >= 0 ? 1 : -1; + } + } + + private LocalDate toLocalDate(Instant instant) { + return LocalDate.ofInstant(instant, zoneId); + } + + private DayOfWeek toDayOfWeek(Instant instant) { + return toLocalDate(instant).getDayOfWeek(); + } + + private enum ZeroDirection { + SUB_DAYS(-1), + ADD_DAYS(1); + + private final int direction; + + ZeroDirection(int direction) { + this.direction = direction; + } + + public int getDirection() { + return direction; + } + } +} diff --git a/lib/taskana-core-test/src/test/java/acceptance/ArchitectureTest.java b/lib/taskana-core-test/src/test/java/acceptance/ArchitectureTest.java index 58d1a19a1a..c3c357e221 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/ArchitectureTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/ArchitectureTest.java @@ -62,6 +62,7 @@ import org.junit.platform.commons.support.AnnotationSupport; import pro.taskana.TaskanaConfiguration; import pro.taskana.common.api.TaskanaEngine; +import pro.taskana.common.api.WorkingTimeCalculator; import pro.taskana.common.api.exceptions.ErrorCode; import pro.taskana.common.api.exceptions.TaskanaException; import pro.taskana.common.api.exceptions.TaskanaRuntimeException; @@ -71,7 +72,6 @@ import pro.taskana.common.internal.jobs.JobScheduler; import pro.taskana.common.internal.logging.LoggingAspect; import pro.taskana.common.internal.workingtime.HolidaySchedule; -import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl; import pro.taskana.testapi.TaskanaIntegrationTest; /** @@ -437,7 +437,7 @@ void classesShouldNotUseWorkingDaysToDaysConverter() { .that() .areNotAssignableFrom(ArchitectureTest.class) .and() - .areNotAssignableTo(WorkingTimeCalculatorImpl.class) + .areNotAssignableTo(WorkingTimeCalculator.class) .and() .areNotAssignableTo(TaskanaEngineImpl.class) .and() diff --git a/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java b/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java index 050e2a0f45..6c94855586 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/TaskanaConfigurationTest.java @@ -251,6 +251,7 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled( Map> expectedClassificationCategories = Map.of("TYPE_A", List.of("CATEGORY_A"), "TYPE_B", List.of("CATEGORY_B")); // working time configuration + boolean expectedUseDetailedWorkingTimeCalculation = false; Map> expectedWorkingTimeSchedule = Map.of(DayOfWeek.MONDAY, Set.of(new LocalTimeInterval(LocalTime.MIN, LocalTime.NOON))); ZoneId expectedWorkingTimeScheduleTimeZone = ZoneId.ofOffset("UTC", ZoneOffset.ofHours(4)); @@ -308,6 +309,7 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled( .classificationTypes(expectedClassificationTypes) .classificationCategoriesByType(expectedClassificationCategories) // working time configuration + .useWorkingTimeCalculation(expectedUseDetailedWorkingTimeCalculation) .workingTimeSchedule(expectedWorkingTimeSchedule) .workingTimeScheduleTimeZone(expectedWorkingTimeScheduleTimeZone) .customHolidays(expectedCustomHolidays) @@ -368,6 +370,8 @@ void should_PopulateEveryTaskanaConfiguration_When_EveryBuilderFunctionIsCalled( assertThat(configuration.getClassificationCategoriesByType()) .isEqualTo(expectedClassificationCategories); // working time configuration + assertThat(configuration.isUseWorkingTimeCalculation()) + .isEqualTo(expectedUseDetailedWorkingTimeCalculation); assertThat(configuration.getWorkingTimeSchedule()).isEqualTo(expectedWorkingTimeSchedule); assertThat(configuration.getWorkingTimeScheduleTimeZone()) .isEqualTo(expectedWorkingTimeScheduleTimeZone); @@ -442,6 +446,7 @@ void should_PopulateEveryConfigurationProperty_When_UsingCopyConstructor() { .classificationCategoriesByType( Map.of("typeA", List.of("categoryA"), "typeB", List.of("categoryB"))) // working time configuration + .useWorkingTimeCalculation(false) .workingTimeSchedule( Map.of( DayOfWeek.MONDAY, diff --git a/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationAccTest.java index 9567993e15..8caadb52f8 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationAccTest.java @@ -46,6 +46,7 @@ import pro.taskana.task.internal.models.TaskImpl; 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; @@ -185,6 +186,50 @@ private List createTasksWithExistingClassificationInAttachment( @Nested class UpdatePriorityAndServiceLevelTest { + @WithAccessId(user = "businessadmin") + @Test + void should_ChangeDueDate_When_ServiceLevelOfClassificationHasChanged() throws Exception { + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P1D") + .buildAndStore(classificationService); + WorkbasketSummary workbasketSummary = + defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummary.getId()) + .accessId(currentUserContext.getUserid()) + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService, "businessadmin"); + + Task task = new TaskBuilder() + .classificationSummary(classification.asSummary()) + .workbasketSummary(workbasketSummary) + .primaryObjRef(defaultTestObjectReference().build()) + .planned(Instant.parse("2021-04-27T15:34:00.000Z")) + .due(null) + .buildAndStore(taskService); + + classificationService.updateClassification(classification); + runAssociatedJobs(); + // read again the task from DB + task = taskService.getTask(task.getId()); + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P1D"); + assertThat(task.getDue()).isAfterOrEqualTo("2021-04-28T15:33:59.999Z"); + + classification.setServiceLevel("P3D"); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + // read again the task from DB + task = taskService.getTask(task.getId()); + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P3D"); + assertThat(task.getDue()).isEqualTo("2021-04-30T15:33:59.999Z"); + } + @WithAccessId(user = "businessadmin") @Test void should_NotThrowException_When_UpdatingClassificationWithEmptyServiceLevel() diff --git a/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationWithWorkingDayCalculatorAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationWithWorkingDayCalculatorAccTest.java new file mode 100644 index 0000000000..c041535993 --- /dev/null +++ b/lib/taskana-core-test/src/test/java/acceptance/classification/update/UpdateClassificationWithWorkingDayCalculatorAccTest.java @@ -0,0 +1,933 @@ +package acceptance.classification.update; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static pro.taskana.testapi.DefaultTestEntities.defaultTestClassification; +import static pro.taskana.testapi.DefaultTestEntities.defaultTestObjectReference; +import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket; +import static pro.taskana.testapi.builder.TaskBuilder.newTask; + +import java.time.Duration; +import java.time.Instant; +import java.time.ZoneId; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestFactory; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.function.ThrowingConsumer; +import pro.taskana.TaskanaConfiguration.Builder; +import pro.taskana.classification.api.ClassificationCustomField; +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.exceptions.ClassificationNotFoundException; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.classification.api.models.ClassificationSummary; +import pro.taskana.classification.internal.models.ClassificationImpl; +import pro.taskana.common.api.TaskanaEngine; +import pro.taskana.common.api.TaskanaRole; +import pro.taskana.common.api.WorkingTimeCalculator; +import pro.taskana.common.api.exceptions.ConcurrencyException; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.api.exceptions.NotAuthorizedException; +import pro.taskana.common.api.security.CurrentUserContext; +import pro.taskana.common.internal.jobs.JobRunner; +import pro.taskana.common.internal.util.Pair; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.models.Attachment; +import pro.taskana.task.api.models.Task; +import pro.taskana.task.internal.models.TaskImpl; +import pro.taskana.testapi.TaskanaConfigurationModifier; +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.models.WorkbasketSummary; + +@TaskanaIntegrationTest +public class UpdateClassificationWithWorkingDayCalculatorAccTest + implements TaskanaConfigurationModifier { + + @TaskanaInject ClassificationService classificationService; + @TaskanaInject TaskanaEngine taskanaEngine; + @TaskanaInject TaskService taskService; + @TaskanaInject WorkbasketService workbasketService; + @TaskanaInject WorkingTimeCalculator workingTimeCalculator; + @TaskanaInject CurrentUserContext currentUserContext; + + @Override + public Builder modify(Builder builder) { + return builder + .workingTimeScheduleTimeZone(ZoneId.of("UTC")) + .useWorkingTimeCalculation(false); // switch to WorkingDayCalculatorImpl + } + + @WithAccessId(user = "businessadmin") + @Test + void should_SetFieldsCorrectly_When_TryingToUpdateClassification() throws Exception { + Classification parentClassification = + defaultTestClassification().buildAndStore(classificationService); + Classification classification = + defaultTestClassification().type("TASK").buildAndStore(classificationService); + final Instant createdBefore = classification.getCreated(); + final Instant modifiedBefore = classification.getModified(); + + classification.setApplicationEntryPoint("newEntrypoint"); + classification.setCategory("PROCESS"); + classification.setCustomField(ClassificationCustomField.CUSTOM_1, "newCustom1"); + classification.setCustomField(ClassificationCustomField.CUSTOM_2, "newCustom2"); + classification.setCustomField(ClassificationCustomField.CUSTOM_3, "newCustom3"); + classification.setCustomField(ClassificationCustomField.CUSTOM_4, "newCustom4"); + classification.setCustomField(ClassificationCustomField.CUSTOM_5, "newCustom5"); + classification.setCustomField(ClassificationCustomField.CUSTOM_6, "newCustom6"); + classification.setCustomField(ClassificationCustomField.CUSTOM_7, "newCustom7"); + classification.setCustomField(ClassificationCustomField.CUSTOM_8, "newCustom8"); + classification.setDescription("newDescription"); + classification.setIsValidInDomain(false); + classification.setName("newName"); + classification.setParentId(parentClassification.getId()); + classification.setParentKey(parentClassification.getKey()); + classification.setPriority(1000); + classification.setServiceLevel("P3D"); + classificationService.updateClassification(classification); + + Classification updatedClassification = + classificationService.getClassification(classification.getKey(), "DOMAIN_A"); + ClassificationImpl expectedClassification = + (ClassificationImpl) + defaultTestClassification() + .type("TASK") + .applicationEntryPoint("newEntrypoint") + .category("PROCESS") + .customAttribute(ClassificationCustomField.CUSTOM_1, "newCustom1") + .customAttribute(ClassificationCustomField.CUSTOM_2, "newCustom2") + .customAttribute(ClassificationCustomField.CUSTOM_3, "newCustom3") + .customAttribute(ClassificationCustomField.CUSTOM_4, "newCustom4") + .customAttribute(ClassificationCustomField.CUSTOM_5, "newCustom5") + .customAttribute(ClassificationCustomField.CUSTOM_6, "newCustom6") + .customAttribute(ClassificationCustomField.CUSTOM_7, "newCustom7") + .customAttribute(ClassificationCustomField.CUSTOM_8, "newCustom8") + .description("newDescription") + .isValidInDomain(false) + .name("newName") + .parentId(parentClassification.getId()) + .parentKey(parentClassification.getKey()) + .priority(1000) + .serviceLevel("P3D") + .created(createdBefore) + .modified(updatedClassification.getModified()) + .buildAndStore(classificationService); + expectedClassification.setKey(updatedClassification.getKey()); + expectedClassification.setId(updatedClassification.getId()); + + assertThat(expectedClassification).hasNoNullFieldsOrProperties(); + assertThat(modifiedBefore).isBefore(classification.getModified()); + assertThat(updatedClassification).isEqualTo(expectedClassification); + } + + private String createTaskWithExistingClassification(ClassificationSummary classificationSummary) + throws Exception { + + WorkbasketSummary workbasketSummary = + defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummary.getId()) + .accessId(currentUserContext.getUserid()) + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService, "businessadmin"); + + return newTask() + .classificationSummary(classificationSummary) + .workbasketSummary(workbasketSummary) + .primaryObjRef(defaultTestObjectReference().build()) + .buildAndStore(taskService) + .getId(); + } + + private List createTasksWithExistingClassificationInAttachment( + ClassificationSummary classificationSummary, String serviceLevel, int priority, int amount) + throws Exception { + List taskList = new ArrayList<>(); + WorkbasketSummary workbasketSummary = + defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummary.getId()) + .accessId(currentUserContext.getUserid()) + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService, "businessadmin"); + ClassificationSummary classificationSummaryWithSpecifiedServiceLevel = + defaultTestClassification() + .serviceLevel(serviceLevel) + .priority(priority) + .buildAndStoreAsSummary(classificationService); + for (int i = 0; i < amount; i++) { + Attachment attachment = taskService.newAttachment(); + attachment.setClassificationSummary(classificationSummary); + attachment.setObjectReference(defaultTestObjectReference().build()); + taskList.add( + newTask() + .classificationSummary(classificationSummaryWithSpecifiedServiceLevel) + .workbasketSummary(workbasketSummary) + .primaryObjRef(defaultTestObjectReference().build()) + .attachments(attachment) + .buildAndStore(taskService) + .getId()); + } + return taskList; + } + + @TestInstance(Lifecycle.PER_CLASS) + @Nested + class UpdatePriorityAndServiceLevelTest { + + @WithAccessId(user = "businessadmin") + @Test + void should_ChangeDueDate_When_ClassificationOfTaskHasChanged() throws Exception { + + final Instant plannedDate = Instant.parse("2021-04-27T15:34:00.000Z"); + final String expectedDue1 = plannedDate.plus(1, ChronoUnit.DAYS).toString(); + final String expectedDue3 = plannedDate.plus(3, ChronoUnit.DAYS).toString(); + + final Classification classificationWithSL1 = + defaultTestClassification() + .priority(1) + .serviceLevel("P1D") + .buildAndStore(classificationService); + final Classification classificationWithSL3 = + defaultTestClassification() + .priority(1) + .serviceLevel("P3D") + .buildAndStore(classificationService); + + WorkbasketSummary workbasketSummary = + defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummary.getId()) + .accessId(currentUserContext.getUserid()) + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.EDITTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService, "businessadmin"); + + Task task = + new TaskBuilder() + .classificationSummary(classificationWithSL1.asSummary()) + .workbasketSummary(workbasketSummary) + .primaryObjRef(defaultTestObjectReference().build()) + .planned(plannedDate) + .due(null) + .buildAndStore(taskService); + + classificationService.updateClassification(classificationWithSL1); + runAssociatedJobs(); + // read again the task from DB + task = taskService.getTask(task.getId()); + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P1D"); + assertThat(task.getDue()).isAfterOrEqualTo(expectedDue1); + + task.setClassificationKey(classificationWithSL3.getKey()); + taskService.updateTask(task); + + // read again the task from DB + task = taskService.getTask(task.getId()); + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P3D"); + assertThat(task.getDue()).isEqualTo(expectedDue3); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_ChangeDueDate_When_ServiceLevelOfClassificationHasChanged() throws Exception { + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P1D") + .buildAndStore(classificationService); + WorkbasketSummary workbasketSummary = + defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(workbasketSummary.getId()) + .accessId(currentUserContext.getUserid()) + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService, "businessadmin"); + + Task task = + new TaskBuilder() + .classificationSummary(classification.asSummary()) + .workbasketSummary(workbasketSummary) + .primaryObjRef(defaultTestObjectReference().build()) + .planned(Instant.parse("2021-04-27T15:34:00.000Z")) + .due(null) + .buildAndStore(taskService); + + classificationService.updateClassification(classification); + runAssociatedJobs(); + // read again the task from DB + task = taskService.getTask(task.getId()); + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P1D"); + assertThat(task.getDue()).isAfterOrEqualTo("2021-04-28T15:34:00.000Z"); + + classification.setServiceLevel("P3D"); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + // read again the task from DB + task = taskService.getTask(task.getId()); + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P3D"); + assertThat(task.getDue()).isEqualTo("2021-04-30T15:34:00.000Z"); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_NotThrowException_When_UpdatingClassificationWithEmptyServiceLevel() + throws Exception { + Classification classification = + defaultTestClassification().serviceLevel("P1D").buildAndStore(classificationService); + classification.setServiceLevel(""); + assertThatCode(() -> classificationService.updateClassification(classification)) + .doesNotThrowAnyException(); + assertThat(classificationService.getClassification(classification.getId()).getServiceLevel()) + .isEqualTo("P0D"); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream + should_SetDefaultServiceLevel_When_TryingToUpdateClassificationWithMissingServiceLevel() + throws Exception { + Classification classification = + defaultTestClassification().serviceLevel("P1D").buildAndStore(classificationService); + List> inputList = + List.of(Pair.of(classification, null), Pair.of(classification, "")); + + ThrowingConsumer> test = + input -> { + input.getLeft().setServiceLevel(input.getRight()); + classificationService.updateClassification(input.getLeft()); + assertThat( + classificationService + .getClassification(input.getLeft().getId()) + .getServiceLevel()) + .isEqualTo("P0D"); + }; + + return DynamicTest.stream( + inputList.iterator(), i -> String.format("for %s", i.getRight()), test); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_UpdateTaskServiceLevel_When_UpdateClassificationInTask() throws Exception { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P13D") + .buildAndStore(classificationService); + final List directLinkedTask = + List.of(createTaskWithExistingClassification(classification.asSummary())); + + classification.setServiceLevel("P15D"); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties(before, directLinkedTask, taskService, workingTimeCalculator, 15, 1); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_UpdateTaskPriority_When_UpdateClassificationInTask() throws Exception { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P13D") + .buildAndStore(classificationService); + final List directLinkedTask = + List.of(createTaskWithExistingClassification(classification.asSummary())); + + classification.setPriority(1000); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, directLinkedTask, taskService, workingTimeCalculator, 13, 1000); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_UpdateTaskPriorityAndServiceLevel_When_UpdateClassificationInTask() + throws Exception { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P13D") + .buildAndStore(classificationService); + final List directLinkedTask = + List.of(createTaskWithExistingClassification(classification.asSummary())); + + classification.setServiceLevel("P15D"); + classification.setPriority(1000); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, directLinkedTask, taskService, workingTimeCalculator, 15, 1000); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream should_UpdateTaskServiceLevel_When_UpdateClassificationInAttachment() { + List> inputs = + List.of(Pair.of("P5D", 2), Pair.of("P8D", 3), Pair.of("P16D", 4)); + + List> outputs = List.of(Pair.of(1, 2), Pair.of(1, 3), Pair.of(1, 4)); + + List, Pair>> zippedTestInputList = + IntStream.range(0, inputs.size()) + .mapToObj(i -> Pair.of(inputs.get(i), outputs.get(i))) + .collect(Collectors.toList()); + + ThrowingConsumer, Pair>> test = + input -> { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P15D") + .buildAndStore(classificationService); + ClassificationSummary classificationSummary = classification.asSummary(); + final List indirectLinkedTasks = + createTasksWithExistingClassificationInAttachment( + classificationSummary, + input.getLeft().getLeft(), + input.getLeft().getRight(), + 5); + + classification.setServiceLevel("P1D"); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, + indirectLinkedTasks, + taskService, + workingTimeCalculator, + input.getRight().getLeft(), + input.getRight().getRight()); + }; + + return DynamicTest.stream( + zippedTestInputList.iterator(), + i -> + String.format( + "for Task with ServiceLevel %s and Priority %s", + i.getLeft().getLeft(), i.getLeft().getRight()), + test); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream should_NotUpdateTaskServiceLevel_When_UpdateClassificationInAttachment() { + List> inputs = + List.of(Pair.of("P5D", 2), Pair.of("P8D", 3), Pair.of("P14D", 4)); + + List> outputs = List.of(Pair.of(5, 2), Pair.of(8, 3), Pair.of(14, 4)); + + List, Pair>> zippedTestInputList = + IntStream.range(0, inputs.size()) + .mapToObj(i -> Pair.of(inputs.get(i), outputs.get(i))) + .collect(Collectors.toList()); + + ThrowingConsumer, Pair>> test = + input -> { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P1D") + .buildAndStore(classificationService); + ClassificationSummary classificationSummary = classification.asSummary(); + final List indirectLinkedTasks = + createTasksWithExistingClassificationInAttachment( + classificationSummary, + input.getLeft().getLeft(), + input.getLeft().getRight(), + 5); + + classification.setServiceLevel("P15D"); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, + indirectLinkedTasks, + taskService, + workingTimeCalculator, + input.getRight().getLeft(), + input.getRight().getRight()); + }; + + return DynamicTest.stream( + zippedTestInputList.iterator(), + i -> + String.format( + "for Task with ServiceLevel %s and Priority %s", + i.getLeft().getLeft(), i.getLeft().getRight()), + test); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream should_UpdateTaskPriority_When_UpdateClassificationInAttachment() { + List> inputs = + List.of(Pair.of("P1D", 1), Pair.of("P8D", 2), Pair.of("P14D", 999)); + + List> outputs = + List.of(Pair.of(1, 1000), Pair.of(8, 1000), Pair.of(14, 1000)); + + List, Pair>> zippedTestInputList = + IntStream.range(0, inputs.size()) + .mapToObj(i -> Pair.of(inputs.get(i), outputs.get(i))) + .collect(Collectors.toList()); + + ThrowingConsumer, Pair>> test = + input -> { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P13D") + .buildAndStore(classificationService); + ClassificationSummary classificationSummary = classification.asSummary(); + final List indirectLinkedTasks = + createTasksWithExistingClassificationInAttachment( + classificationSummary, + input.getLeft().getLeft(), + input.getLeft().getRight(), + 5); + + classification.setServiceLevel("P15D"); + classification.setPriority(1000); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, + indirectLinkedTasks, + taskService, + workingTimeCalculator, + input.getRight().getLeft(), + input.getRight().getRight()); + }; + + return DynamicTest.stream( + zippedTestInputList.iterator(), + i -> + String.format( + "for Task with ServiceLevel %s and Priority %s", + i.getLeft().getLeft(), i.getLeft().getRight()), + test); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream should_NotUpdateTaskPriority_When_UpdateClassificationInAttachment() { + List> inputs = + List.of(Pair.of("P1D", 2), Pair.of("P8D", 3), Pair.of("P14D", 999)); + + List> outputs = + List.of(Pair.of(1, 2), Pair.of(8, 3), Pair.of(14, 999)); + + List, Pair>> zippedTestInputList = + IntStream.range(0, inputs.size()) + .mapToObj(i -> Pair.of(inputs.get(i), outputs.get(i))) + .collect(Collectors.toList()); + + ThrowingConsumer, Pair>> test = + input -> { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1000) + .serviceLevel("P13D") + .buildAndStore(classificationService); + ClassificationSummary classificationSummary = classification.asSummary(); + final List indirectLinkedTasks = + createTasksWithExistingClassificationInAttachment( + classificationSummary, + input.getLeft().getLeft(), + input.getLeft().getRight(), + 5); + + classification.setServiceLevel("P15D"); + classification.setPriority(1); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, + indirectLinkedTasks, + taskService, + workingTimeCalculator, + input.getRight().getLeft(), + input.getRight().getRight()); + }; + + return DynamicTest.stream( + zippedTestInputList.iterator(), + i -> + String.format( + "for Task with ServiceLevel %s and Priority %s", + i.getLeft().getLeft(), i.getLeft().getRight()), + test); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream + should_UpdateTaskPriorityAndServiceLevel_When_UpdateClassificationInAttachment() { + List> inputs = List.of(Pair.of("P1D", 5), Pair.of("P14D", 98)); + + List> outputs = List.of(Pair.of(1, 99), Pair.of(1, 99)); + + List, Pair>> zippedTestInputList = + IntStream.range(0, inputs.size()) + .mapToObj(i -> Pair.of(inputs.get(i), outputs.get(i))) + .collect(Collectors.toList()); + + ThrowingConsumer, Pair>> test = + input -> { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1) + .serviceLevel("P13D") + .buildAndStore(classificationService); + ClassificationSummary classificationSummary = classification.asSummary(); + final List indirectLinkedTasks = + createTasksWithExistingClassificationInAttachment( + classificationSummary, + input.getLeft().getLeft(), + input.getLeft().getRight(), + 3); + + classification.setServiceLevel("P1D"); + classification.setPriority(99); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, + indirectLinkedTasks, + taskService, + workingTimeCalculator, + input.getRight().getLeft(), + input.getRight().getRight()); + }; + + return DynamicTest.stream( + zippedTestInputList.iterator(), + i -> + String.format( + "for Task with ServiceLevel %s and Priority %s", + i.getLeft().getLeft(), i.getLeft().getRight()), + test); + } + + @WithAccessId(user = "businessadmin") + @TestFactory + Stream + should_NotUpdateTaskPriorityAndServiceLevel_When_UpdateClassificationInAttachment() { + List> inputs = List.of(Pair.of("P1D", 5), Pair.of("P14D", 98)); + + List> outputs = List.of(Pair.of(1, 5), Pair.of(14, 98)); + + List, Pair>> zippedTestInputList = + IntStream.range(0, inputs.size()) + .mapToObj(i -> Pair.of(inputs.get(i), outputs.get(i))) + .collect(Collectors.toList()); + + ThrowingConsumer, Pair>> test = + input -> { + final Instant before = Instant.now(); + Classification classification = + defaultTestClassification() + .priority(1000) + .serviceLevel("P1D") + .buildAndStore(classificationService); + ClassificationSummary classificationSummary = classification.asSummary(); + final List indirectLinkedTasks = + createTasksWithExistingClassificationInAttachment( + classificationSummary, + input.getLeft().getLeft(), + input.getLeft().getRight(), + 3); + + classification.setServiceLevel("P15D"); + classification.setPriority(1); + classificationService.updateClassification(classification); + runAssociatedJobs(); + + validateTaskProperties( + before, + indirectLinkedTasks, + taskService, + workingTimeCalculator, + input.getRight().getLeft(), + input.getRight().getRight()); + }; + + return DynamicTest.stream( + zippedTestInputList.iterator(), + i -> + String.format( + "for Task with ServiceLevel %s and Priority %s", + i.getLeft().getLeft(), i.getLeft().getRight()), + test); + } + + private void runAssociatedJobs() throws Exception { + Thread.sleep(10); + // run the ClassificationChangedJob + JobRunner runner = new JobRunner(taskanaEngine); + // run the TaskRefreshJob that was scheduled by the ClassificationChangedJob. + runner.runJobs(); + Thread.sleep( + 10); // otherwise the next runJobs call intermittently doesn't find the Job created + // by the previous step (it searches with DueDate < CurrentTime) + runner.runJobs(); + } + + private void validateTaskProperties( + Instant before, + List tasksUpdated, + TaskService taskService, + WorkingTimeCalculator workingTimeCalculator, + int serviceLevel, + int priority) + throws Exception { + for (String taskId : tasksUpdated) { + Task task = taskService.getTask(taskId); + + Instant expDue = + workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(serviceLevel)); + assertThat(task.getModified()) + .describedAs("Task " + task.getId() + " has not been refreshed.") + .isAfter(before); + assertThat(task.getDue()).isEqualTo(expDue); + assertThat(task.getPriority()).isEqualTo(priority); + } + } + } + + @TestInstance(Lifecycle.PER_CLASS) + @Nested + class UpdateClassificationExceptionTest { + /** + * This BeforeAll method is needed for this {@linkplain + * #should_ThrowException_When_UserIsNotAuthorized test} and {@linkplain + * #should_ThrowException_When_UserRoleIsNotAdminOrBusinessAdmin test} since it can't create an + * own classification. + * + * @throws Exception for errors in the building or reading process of entities. + */ + @WithAccessId(user = "businessadmin") + @BeforeAll + void createClassifications() throws Exception { + defaultTestClassification() + .key("BeforeAllClassification") + .buildAndStore(classificationService); + } + + @Test + void should_ThrowException_When_UserIsNotAuthorized() throws Exception { + Classification classification = + classificationService.getClassification("BeforeAllClassification", "DOMAIN_A"); + classification.setCustomField(ClassificationCustomField.CUSTOM_1, "newCustom1"); + + NotAuthorizedException expectedException = + new NotAuthorizedException( + currentUserContext.getUserid(), TaskanaRole.BUSINESS_ADMIN, TaskanaRole.ADMIN); + assertThatThrownBy(() -> classificationService.updateClassification(classification)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + + @WithAccessId(user = "taskadmin") + @WithAccessId(user = "user-1-1") + @TestTemplate + void should_ThrowException_When_UserRoleIsNotAdminOrBusinessAdmin() throws Exception { + Classification classification = + classificationService.getClassification("BeforeAllClassification", "DOMAIN_A"); + + classification.setApplicationEntryPoint("updated EntryPoint"); + classification.setName("updated Name"); + + NotAuthorizedException expectedException = + new NotAuthorizedException( + currentUserContext.getUserid(), TaskanaRole.BUSINESS_ADMIN, TaskanaRole.ADMIN); + assertThatThrownBy(() -> classificationService.updateClassification(classification)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_ThrowException_When_UpdatingClassificationConcurrently() throws Exception { + Classification classification = + defaultTestClassification().buildAndStore(classificationService); + final Classification classificationSecondUpdate = + classificationService.getClassification( + classification.getKey(), classification.getDomain()); + + classification.setApplicationEntryPoint("Application Entry Point"); + classification.setDescription("Description"); + classification.setName("Name"); + Thread.sleep(20); // to avoid identity of modified timestamps between classification and base + classificationService.updateClassification(classification); + classificationSecondUpdate.setName("Name again"); + classificationSecondUpdate.setDescription("Description again"); + + ConcurrencyException expectedException = + new ConcurrencyException(classificationSecondUpdate.getId()); + assertThatThrownBy( + () -> classificationService.updateClassification(classificationSecondUpdate)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_ThrowException_When_TryingToUpdateClassificationWithInvalidParentId() + throws Exception { + Classification classification = + defaultTestClassification().buildAndStore(classificationService); + + classification.setParentId("NON EXISTING ID"); + + ClassificationNotFoundException expectedException = + new ClassificationNotFoundException("NON EXISTING ID"); + assertThatThrownBy(() -> classificationService.updateClassification(classification)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_ThrowException_When_TryingToUpdateClassificationWithInvalidParentKey() + throws Exception { + Classification classification = + defaultTestClassification().buildAndStore(classificationService); + + classification.setParentKey("NON EXISTING KEY"); + + ClassificationNotFoundException expectedException = + new ClassificationNotFoundException("NON EXISTING KEY", "DOMAIN_A"); + assertThatThrownBy(() -> classificationService.updateClassification(classification)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_ThrowException_When_TryingToUpdateClassificationWithOwnKeyAsParentKey() + throws Exception { + Classification classification = + defaultTestClassification().buildAndStore(classificationService); + + classification.setParentKey(classification.getKey()); + + InvalidArgumentException expectedException = + new InvalidArgumentException( + String.format( + "The Classification '%s' has the same key and parent key", + classification.getName())); + assertThatThrownBy(() -> classificationService.updateClassification(classification)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + } + + @TestInstance(Lifecycle.PER_CLASS) + @Nested + class UpdateClassificationCategoryTest { + Classification classification; + Task task; + Instant createdBefore; + Instant modifiedBefore; + + @WithAccessId(user = "businessadmin") + @BeforeEach + void createClassificationAndTask() throws Exception { + classification = + defaultTestClassification() + .category("MANUAL") + .type("TASK") + .buildAndStore(classificationService); + createdBefore = classification.getCreated(); + modifiedBefore = classification.getModified(); + String taskId = createTaskWithExistingClassification(classification.asSummary()); + task = taskService.getTask(taskId); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_UpdateTask_When_UpdatingClassificationCategory() throws Exception { + classification.setCategory("PROCESS"); + classificationService.updateClassification(classification); + final Task updatedTask = taskService.getTask(task.getId()); + + TaskImpl expectedUpdatedTask = (TaskImpl) task.copy(); + expectedUpdatedTask.setId(task.getId()); + expectedUpdatedTask.setClassificationCategory("PROCESS"); + expectedUpdatedTask.setClassificationSummary( + classificationService.getClassification(classification.getId()).asSummary()); + expectedUpdatedTask.setExternalId(task.getExternalId()); + assertThat(expectedUpdatedTask) + .usingRecursiveComparison() + .ignoringFields("modified") + .isEqualTo(updatedTask); + assertThat(expectedUpdatedTask.getModified()).isAfterOrEqualTo(modifiedBefore); + } + + @WithAccessId(user = "businessadmin") + @Test + void should_UpdateClassification_When_UpdatingClassificationCategory() throws Exception { + classification.setCategory("PROCESS"); + classificationService.updateClassification(classification); + + Classification updatedClassification = + classificationService.getClassification(classification.getId()); + assertThat(updatedClassification) + .usingRecursiveComparison() + .ignoringFields("modified") + .isEqualTo(classification); + assertThat(updatedClassification.getModified()).isAfterOrEqualTo(modifiedBefore); + } + } +} diff --git a/lib/taskana-core-test/src/test/java/acceptance/task/ServiceLevelOfAllTasksAccTest.java b/lib/taskana-core-test/src/test/java/acceptance/task/ServiceLevelOfAllTasksAccTest.java index 5e0ca19c92..f49d5ebd49 100644 --- a/lib/taskana-core-test/src/test/java/acceptance/task/ServiceLevelOfAllTasksAccTest.java +++ b/lib/taskana-core-test/src/test/java/acceptance/task/ServiceLevelOfAllTasksAccTest.java @@ -5,18 +5,26 @@ import static pro.taskana.testapi.DefaultTestEntities.defaultTestObjectReference; import static pro.taskana.testapi.DefaultTestEntities.defaultTestWorkbasket; +import java.time.Duration; import java.time.Instant; import java.util.List; import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.TestInstance.Lifecycle; +import pro.taskana.TaskanaConfiguration.Builder; import pro.taskana.classification.api.ClassificationService; import pro.taskana.classification.api.models.ClassificationSummary; import pro.taskana.common.api.BulkOperationResults; +import pro.taskana.common.api.TaskanaEngine; +import pro.taskana.common.api.WorkingTimeCalculator; import pro.taskana.common.api.exceptions.TaskanaException; import pro.taskana.task.api.TaskService; import pro.taskana.task.api.models.Attachment; import pro.taskana.task.api.models.ObjectReference; import pro.taskana.task.api.models.TaskSummary; +import pro.taskana.testapi.TaskanaConfigurationModifier; import pro.taskana.testapi.TaskanaInject; import pro.taskana.testapi.TaskanaIntegrationTest; import pro.taskana.testapi.builder.TaskAttachmentBuilder; @@ -37,172 +45,364 @@ class ServiceLevelOfAllTasksAccTest { private static final String SMALL_CLASSIFICATION_SERVICE_LEVEL = "P2D"; private static final String GREAT_CLASSIFICATION_SERVICE_LEVEL = "P7D"; - @TaskanaInject TaskService taskService; - @TaskanaInject WorkbasketService workbasketService; - @TaskanaInject ClassificationService classificationService; - - ClassificationSummary classificationSummarySmallServiceLevel; - ClassificationSummary classificationSummaryGreatServiceLevel; - Attachment attachmentSummarySmallServiceLevel; - Attachment attachmentSummaryGreatServiceLevel; - WorkbasketSummary defaultWorkbasketSummary; - ObjectReference defaultObjectReference; - - @WithAccessId(user = "businessadmin") - @BeforeAll - void setup() throws Exception { - classificationSummarySmallServiceLevel = - defaultTestClassification() - .serviceLevel(SMALL_CLASSIFICATION_SERVICE_LEVEL) - .buildAndStoreAsSummary(classificationService); - classificationSummaryGreatServiceLevel = - defaultTestClassification() - .serviceLevel(GREAT_CLASSIFICATION_SERVICE_LEVEL) - .buildAndStoreAsSummary(classificationService); - - defaultObjectReference = defaultTestObjectReference().build(); - - attachmentSummarySmallServiceLevel = - TaskAttachmentBuilder.newAttachment() - .classificationSummary(classificationSummarySmallServiceLevel) - .objectReference(defaultObjectReference) - .build(); - attachmentSummaryGreatServiceLevel = - TaskAttachmentBuilder.newAttachment() - .classificationSummary(classificationSummaryGreatServiceLevel) - .objectReference(defaultObjectReference) - .build(); - - defaultWorkbasketSummary = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); - WorkbasketAccessItemBuilder.newWorkbasketAccessItem() - .workbasketId(defaultWorkbasketSummary.getId()) - .accessId("user-1-1") - .permission(WorkbasketPermission.OPEN) - .permission(WorkbasketPermission.READ) - .permission(WorkbasketPermission.READTASKS) - .permission(WorkbasketPermission.APPEND) - .buildAndStore(workbasketService); - } + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class WithWorkingTimeCalculation { + @TaskanaInject TaskService taskService; + @TaskanaInject WorkbasketService workbasketService; + @TaskanaInject ClassificationService classificationService; - @WithAccessId(user = "user-1-1") - @Test - void should_SetPlannedOnMultipleTasks() throws Exception { - Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); - TaskSummary task1 = - createDefaultTask() - .classificationSummary(classificationSummarySmallServiceLevel) - .buildAndStoreAsSummary(taskService); - TaskSummary task2 = - createDefaultTask() - .classificationSummary(classificationSummarySmallServiceLevel) - .attachments(attachmentSummaryGreatServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - TaskSummary task3 = - createDefaultTask() - .classificationSummary(classificationSummaryGreatServiceLevel) - .attachments(attachmentSummarySmallServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - List taskIds = List.of(task1.getId(), task2.getId(), task3.getId()); - - BulkOperationResults bulkLog = - taskService.setPlannedPropertyOfTasks(planned, taskIds); - - assertThat(bulkLog.containsErrors()).isFalse(); - List result = - taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list(); - assertThat(result).extracting(TaskSummary::getPlanned).containsOnly(planned); - } + ClassificationSummary classificationSummarySmallServiceLevel; + ClassificationSummary classificationSummaryGreatServiceLevel; + Attachment attachmentSummarySmallServiceLevel; + Attachment attachmentSummaryGreatServiceLevel; + WorkbasketSummary defaultWorkbasketSummary; + ObjectReference defaultObjectReference; - @WithAccessId(user = "user-1-1") - @Test - void should_ChangeDue_When_SettingPlannedAndClassificationHasSmallerServiceLevel() - throws Exception { - Instant planned = Instant.parse("2020-05-04T07:00:00.000Z"); - TaskSummary task1 = - createDefaultTask() - .classificationSummary(classificationSummarySmallServiceLevel) - .attachments(attachmentSummaryGreatServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - TaskSummary task2 = - createDefaultTask() - .classificationSummary(classificationSummarySmallServiceLevel) - .buildAndStoreAsSummary(taskService); - List taskIds = List.of(task1.getId(), task2.getId()); - - BulkOperationResults bulkLog = - taskService.setPlannedPropertyOfTasks(planned, taskIds); - - assertThat(bulkLog.containsErrors()).isFalse(); - List result = - taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list(); - Instant expectedDue = Instant.parse("2020-05-06T06:59:59.999Z"); - assertThat(result).extracting(TaskSummary::getDue).containsOnly(expectedDue); - } + @WithAccessId(user = "businessadmin") + @BeforeAll + void setup() throws Exception { + classificationSummarySmallServiceLevel = + defaultTestClassification() + .serviceLevel(SMALL_CLASSIFICATION_SERVICE_LEVEL) + .buildAndStoreAsSummary(classificationService); + classificationSummaryGreatServiceLevel = + defaultTestClassification() + .serviceLevel(GREAT_CLASSIFICATION_SERVICE_LEVEL) + .buildAndStoreAsSummary(classificationService); - @WithAccessId(user = "user-1-1") - @Test - void should_ChangeDue_When_SettingPlannedAndAttachmentHasSmallerOrEqualServiceLevel() - throws Exception { - Instant planned = Instant.parse("2020-05-04T07:00:00.000Z"); - TaskSummary task1 = - createDefaultTask() - .classificationSummary(classificationSummaryGreatServiceLevel) - .attachments(attachmentSummarySmallServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - TaskSummary task2 = - createDefaultTask() - .classificationSummary(classificationSummarySmallServiceLevel) - .attachments(attachmentSummarySmallServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - List taskIds = List.of(task1.getId(), task2.getId()); - - BulkOperationResults bulkLog = - taskService.setPlannedPropertyOfTasks(planned, taskIds); - - assertThat(bulkLog.containsErrors()).isFalse(); - List result = - taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list(); - Instant expectedDue = Instant.parse("2020-05-06T06:59:59.999Z"); - assertThat(result).extracting(TaskSummary::getDue).containsOnly(expectedDue); - } + defaultObjectReference = defaultTestObjectReference().build(); + + attachmentSummarySmallServiceLevel = + TaskAttachmentBuilder.newAttachment() + .classificationSummary(classificationSummarySmallServiceLevel) + .objectReference(defaultObjectReference) + .build(); + attachmentSummaryGreatServiceLevel = + TaskAttachmentBuilder.newAttachment() + .classificationSummary(classificationSummaryGreatServiceLevel) + .objectReference(defaultObjectReference) + .build(); + + defaultWorkbasketSummary = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(defaultWorkbasketSummary.getId()) + .accessId("user-1-1") + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_SetPlannedOnMultipleTasks() throws Exception { + Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task3 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId(), task3.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list(); + assertThat(result).extracting(TaskSummary::getPlanned).containsOnly(planned); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ChangeDue_When_SettingPlannedAndClassificationHasSmallerServiceLevel() + throws Exception { + Instant planned = Instant.parse("2020-05-04T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId()); - @WithAccessId(user = "user-1-1") - @Test - void should_ChangeDue_When_SettingPlannedAndUsingDifferentServiceLevels() throws Exception { - Instant planned = Instant.parse("2020-05-04T07:00:00.000Z"); - TaskSummary task1 = - createDefaultTask() - .classificationSummary(classificationSummaryGreatServiceLevel) - .attachments(attachmentSummarySmallServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - TaskSummary task2 = - createDefaultTask() - .classificationSummary(classificationSummarySmallServiceLevel) - .attachments(attachmentSummaryGreatServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - TaskSummary task3 = - createDefaultTask() - .classificationSummary(classificationSummaryGreatServiceLevel) - .attachments(attachmentSummaryGreatServiceLevel.copy()) - .buildAndStoreAsSummary(taskService); - List taskIds = List.of(task1.getId(), task2.getId(), task3.getId()); - - BulkOperationResults bulkLog = - taskService.setPlannedPropertyOfTasks(planned, taskIds); - - assertThat(bulkLog.containsErrors()).isFalse(); - List result = - taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list(); - Instant expectedDueSmallServiceLevel = Instant.parse("2020-05-06T06:59:59.999Z"); - Instant expectedDueGreatServiceLevel = Instant.parse("2020-05-13T06:59:59.999Z"); - assertThat(result) - .extracting(TaskSummary::getDue) - .containsOnly(expectedDueSmallServiceLevel, expectedDueGreatServiceLevel); + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list(); + Instant expectedDue = Instant.parse("2020-05-06T06:59:59.999Z"); + assertThat(result).extracting(TaskSummary::getDue).containsOnly(expectedDue); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ChangeDue_When_SettingPlannedAndAttachmentHasSmallerOrEqualServiceLevel() + throws Exception { + Instant planned = Instant.parse("2020-05-04T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list(); + Instant expectedDue = Instant.parse("2020-05-06T06:59:59.999Z"); + assertThat(result).extracting(TaskSummary::getDue).containsOnly(expectedDue); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ChangeDue_When_SettingPlannedAndUsingDifferentServiceLevels() throws Exception { + Instant planned = Instant.parse("2020-05-04T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task3 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId(), task3.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list(); + Instant expectedDueSmallServiceLevel = Instant.parse("2020-05-06T06:59:59.999Z"); + Instant expectedDueGreatServiceLevel = Instant.parse("2020-05-13T06:59:59.999Z"); + assertThat(result) + .extracting(TaskSummary::getDue) + .containsOnly(expectedDueSmallServiceLevel, expectedDueGreatServiceLevel); + } + + private TaskBuilder createDefaultTask() { + return (TaskBuilder.newTask() + .workbasketSummary(defaultWorkbasketSummary) + .primaryObjRef(defaultObjectReference)); + } } - private TaskBuilder createDefaultTask() { - return (TaskBuilder.newTask() - .workbasketSummary(defaultWorkbasketSummary) - .primaryObjRef(defaultObjectReference)); + @Nested + @TestInstance(Lifecycle.PER_CLASS) + class WithWorkingDaysCalculation implements TaskanaConfigurationModifier { + + @TaskanaInject TaskanaEngine taskanaEngine; + @TaskanaInject TaskService taskService; + @TaskanaInject WorkbasketService workbasketService; + @TaskanaInject ClassificationService classificationService; + ClassificationSummary classificationSummarySmallServiceLevel; + ClassificationSummary classificationSummaryGreatServiceLevel; + Attachment attachmentSummarySmallServiceLevel; + Attachment attachmentSummaryGreatServiceLevel; + WorkbasketSummary defaultWorkbasketSummary; + ObjectReference defaultObjectReference; + WorkingTimeCalculator converter; + + @Override + public Builder modify(Builder builder) { + return builder.useWorkingTimeCalculation(false); + } + + @WithAccessId(user = "businessadmin") + @BeforeAll + void setup() throws Exception { + classificationSummarySmallServiceLevel = + defaultTestClassification() + .serviceLevel(SMALL_CLASSIFICATION_SERVICE_LEVEL) + .buildAndStoreAsSummary(classificationService); + classificationSummaryGreatServiceLevel = + defaultTestClassification() + .serviceLevel(GREAT_CLASSIFICATION_SERVICE_LEVEL) + .buildAndStoreAsSummary(classificationService); + + defaultObjectReference = defaultTestObjectReference().build(); + + attachmentSummarySmallServiceLevel = + TaskAttachmentBuilder.newAttachment() + .classificationSummary(classificationSummarySmallServiceLevel) + .objectReference(defaultObjectReference) + .build(); + attachmentSummaryGreatServiceLevel = + TaskAttachmentBuilder.newAttachment() + .classificationSummary(classificationSummaryGreatServiceLevel) + .objectReference(defaultObjectReference) + .build(); + + defaultWorkbasketSummary = defaultTestWorkbasket().buildAndStoreAsSummary(workbasketService); + WorkbasketAccessItemBuilder.newWorkbasketAccessItem() + .workbasketId(defaultWorkbasketSummary.getId()) + .accessId("user-1-1") + .permission(WorkbasketPermission.OPEN) + .permission(WorkbasketPermission.READ) + .permission(WorkbasketPermission.READTASKS) + .permission(WorkbasketPermission.APPEND) + .buildAndStore(workbasketService); + converter = taskanaEngine.getWorkingTimeCalculator(); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_SetPlannedOnMultipleTasks() throws Exception { + Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task3 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId(), task3.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list(); + assertThat(result).extracting(TaskSummary::getPlanned).containsOnly(planned); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ChangeDue_When_SettingPlannedAndClassificationHasSmallerServiceLevel() + throws Exception { + Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list(); + assertThat(result) + .extracting(TaskSummary::getDue) + .containsOnly( + converter.addWorkingTime( + planned, Duration.parse(SMALL_CLASSIFICATION_SERVICE_LEVEL))); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ChangeDue_When_SettingPlannedAndAttachmentHasSmallerOrEqualServiceLevel() + throws Exception { + Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId()).list(); + assertThat(result) + .extracting(TaskSummary::getDue) + .containsOnly( + converter.addWorkingTime( + planned, Duration.parse(SMALL_CLASSIFICATION_SERVICE_LEVEL))); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ChangeDue_When_SettingPlannedAndUsingDifferentServiceLevels() throws Exception { + Instant planned = Instant.parse("2020-05-03T07:00:00.000Z"); + TaskSummary task1 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummarySmallServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task2 = + createDefaultTask() + .classificationSummary(classificationSummarySmallServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + TaskSummary task3 = + createDefaultTask() + .classificationSummary(classificationSummaryGreatServiceLevel) + .attachments(attachmentSummaryGreatServiceLevel.copy()) + .buildAndStoreAsSummary(taskService); + List taskIds = List.of(task1.getId(), task2.getId(), task3.getId()); + + BulkOperationResults bulkLog = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(bulkLog.containsErrors()).isFalse(); + List result = + taskService.createTaskQuery().idIn(task1.getId(), task2.getId(), task3.getId()).list(); + assertThat(result) + .extracting(TaskSummary::getDue) + .containsOnly( + converter.addWorkingTime(planned, Duration.parse(SMALL_CLASSIFICATION_SERVICE_LEVEL)), + converter.addWorkingTime( + planned, Duration.parse(GREAT_CLASSIFICATION_SERVICE_LEVEL))); + } + + private TaskBuilder createDefaultTask() { + return (TaskBuilder.newTask() + .workbasketSummary(defaultWorkbasketSummary) + .primaryObjRef(defaultObjectReference)); + } } } diff --git a/lib/taskana-core-test/src/test/resources/fullTaskana.properties b/lib/taskana-core-test/src/test/resources/fullTaskana.properties index a3b672bd25..83332a8715 100644 --- a/lib/taskana-core-test/src/test/resources/fullTaskana.properties +++ b/lib/taskana-core-test/src/test/resources/fullTaskana.properties @@ -13,6 +13,7 @@ taskana.classification.types=TASK | document taskana.classification.categories.task=EXTERNAL| manual| autoMAtic| Process taskana.classification.categories.document=EXTERNAL # working time configuration +taskana.workingTime.useWorkingTimeCalculation=false taskana.workingTime.schedule.MONDAY=09:00-18:00 taskana.workingTime.schedule.TUESDAY=09:00-18:00 taskana.workingTime.schedule.WEDNESDAY=09:00-18:00 diff --git a/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java b/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java index a4ce66fce6..c98c3de678 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java +++ b/lib/taskana-core/src/main/java/pro/taskana/TaskanaConfiguration.java @@ -73,6 +73,7 @@ public class TaskanaConfiguration { // endregion // region working time configuration + private final boolean useWorkingTimeCalculation; private final Map> workingTimeSchedule; private final ZoneId workingTimeScheduleTimeZone; private final Set customHolidays; @@ -153,6 +154,7 @@ private TaskanaConfiguration(Builder builder) { Collectors.toUnmodifiableMap( Entry::getKey, e -> Collections.unmodifiableList(e.getValue()))); // working time configuration + this.useWorkingTimeCalculation = builder.useWorkingTimeCalculation; this.workingTimeSchedule = builder.workingTimeSchedule.entrySet().stream() .collect( @@ -262,6 +264,10 @@ public List getClassificationTypes() { return classificationTypes; } + public boolean isUseWorkingTimeCalculation() { + return useWorkingTimeCalculation; + } + public Map> getWorkingTimeSchedule() { return workingTimeSchedule; } @@ -424,6 +430,7 @@ public int hashCode() { roleMap, classificationTypes, classificationCategoriesByType, + useWorkingTimeCalculation, workingTimeSchedule, workingTimeScheduleTimeZone, customHolidays, @@ -473,6 +480,7 @@ public boolean equals(Object obj) { return useManagedTransactions == other.useManagedTransactions && securityEnabled == other.securityEnabled && enforceServiceLevel == other.enforceServiceLevel + && useWorkingTimeCalculation == other.useWorkingTimeCalculation && germanPublicHolidaysEnabled == other.germanPublicHolidaysEnabled && germanPublicHolidaysCorpusChristiEnabled == other.germanPublicHolidaysCorpusChristiEnabled @@ -542,6 +550,8 @@ public String toString() { + classificationTypes + ", classificationCategoriesByType=" + classificationCategoriesByType + + ", useWorkingTimeCalculation=" + + useWorkingTimeCalculation + ", workingTimeSchedule=" + workingTimeSchedule + ", workingTimeScheduleTimeZone=" @@ -650,6 +660,10 @@ public static class Builder { // endregion // region working time configuration + + @TaskanaProperty("taskana.workingTime.useWorkingTimeCalculation") + private boolean useWorkingTimeCalculation = true; + @TaskanaProperty("taskana.workingTime.schedule") private Map> workingTimeSchedule = initDefaultWorkingTimeSchedule(); @@ -826,6 +840,7 @@ public Builder( this.classificationTypes = conf.classificationTypes; this.classificationCategoriesByType = conf.classificationCategoriesByType; // working time configuration + this.useWorkingTimeCalculation = conf.useWorkingTimeCalculation; this.workingTimeSchedule = conf.workingTimeSchedule; this.workingTimeScheduleTimeZone = conf.workingTimeScheduleTimeZone; this.customHolidays = conf.customHolidays; @@ -964,6 +979,11 @@ public Builder classificationCategoriesByType( // region working time configuration + public Builder useWorkingTimeCalculation(boolean useWorkingTimeCalculation) { + this.useWorkingTimeCalculation = useWorkingTimeCalculation; + return this; + } + public Builder workingTimeSchedule(Map> workingTimeSchedule) { this.workingTimeSchedule = workingTimeSchedule; return this; diff --git a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java index 0220d50365..f59863ad69 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java +++ b/lib/taskana-core/src/main/java/pro/taskana/common/internal/TaskanaEngineImpl.java @@ -51,6 +51,7 @@ import pro.taskana.common.internal.persistence.StringTypeHandler; import pro.taskana.common.internal.security.CurrentUserContextImpl; import pro.taskana.common.internal.workingtime.HolidaySchedule; +import pro.taskana.common.internal.workingtime.WorkingDayCalculatorImpl; import pro.taskana.common.internal.workingtime.WorkingTimeCalculatorImpl; import pro.taskana.monitor.api.MonitorService; import pro.taskana.monitor.internal.MonitorMapper; @@ -128,11 +129,18 @@ protected TaskanaEngineImpl( taskanaConfiguration.isGermanPublicHolidaysEnabled(), taskanaConfiguration.isGermanPublicHolidaysCorpusChristiEnabled(), taskanaConfiguration.getCustomHolidays()); - workingTimeCalculator = - new WorkingTimeCalculatorImpl( - holidaySchedule, - taskanaConfiguration.getWorkingTimeSchedule(), - taskanaConfiguration.getWorkingTimeScheduleTimeZone()); + if (taskanaConfiguration.isUseWorkingTimeCalculation()) { + workingTimeCalculator = + new WorkingTimeCalculatorImpl( + holidaySchedule, + taskanaConfiguration.getWorkingTimeSchedule(), + taskanaConfiguration.getWorkingTimeScheduleTimeZone()); + } else { + workingTimeCalculator = + new WorkingDayCalculatorImpl( + holidaySchedule, taskanaConfiguration.getWorkingTimeScheduleTimeZone()); + } + currentUserContext = new CurrentUserContextImpl(TaskanaConfiguration.shouldUseLowerCaseForAccessIds()); createTransactionFactory(taskanaConfiguration.isUseManagedTransactions()); diff --git a/lib/taskana-core/src/main/java/pro/taskana/task/internal/ServiceLevelHandler.java b/lib/taskana-core/src/main/java/pro/taskana/task/internal/ServiceLevelHandler.java index 5f140930b8..028bd70a78 100644 --- a/lib/taskana-core/src/main/java/pro/taskana/task/internal/ServiceLevelHandler.java +++ b/lib/taskana-core/src/main/java/pro/taskana/task/internal/ServiceLevelHandler.java @@ -303,7 +303,8 @@ private boolean plannedHasChanged(Task newTask, Task oldTask) { private Instant calculateDue(Instant planned, Duration duration) { Instant dueExclusive = workingTimeCalculator.addWorkingTime(planned, duration); - if (!planned.equals(dueExclusive)) { + if (taskanaEngine.getEngine().getConfiguration().isUseWorkingTimeCalculation() + && !planned.equals(dueExclusive)) { // Calculation is exclusive, but we want due date to be inclusive. Hence, we subtract a // millisecond // If planned and dueExclusive are the same values, we don't want due to be before planned. @@ -320,7 +321,11 @@ private Instant calculatePlanned(Instant due, Duration duration) { return due; } else { // due is inclusive, but calculation happens exclusive. - return workingTimeCalculator.subtractWorkingTime(due.plusMillis(1), duration); + Instant normalize = + taskanaEngine.getEngine().getConfiguration().isUseWorkingTimeCalculation() + ? due.plusMillis(1) + : due; + return workingTimeCalculator.subtractWorkingTime(normalize, duration); } } @@ -328,9 +333,13 @@ private Instant normalizeDue(Instant due) { // plusMillis since due is inclusive, but calculation happens exclusive. // minusMillis since we calculated a due date // Without that some edge case fail (e.g. due is exactly the start of weekend) - return workingTimeCalculator - .subtractWorkingTime(due.plusMillis(1), Duration.ZERO) - .minusMillis(1); + if (taskanaEngine.getEngine().getConfiguration().isUseWorkingTimeCalculation()) { + return workingTimeCalculator + .subtractWorkingTime(due.plusMillis(1), Duration.ZERO) + .minusMillis(1); + } + + return workingTimeCalculator.subtractWorkingTime(due, Duration.ZERO); } private Instant normalizePlanned(Instant instant) { @@ -625,9 +634,9 @@ private boolean isPriorityAndDurationAlreadyCorrect(TaskImpl newTaskImpl, TaskIm } // TODO Do we need to compare Key and Id or could we simply compare ClassificationSummary only? final boolean isClassificationKeyChanged = - Objects.equals(newTaskImpl.getClassificationKey(), oldTaskImpl.getClassificationKey()); + !Objects.equals(newTaskImpl.getClassificationKey(), oldTaskImpl.getClassificationKey()); final boolean isClassificationIdChanged = - Objects.equals(newTaskImpl.getClassificationId(), oldTaskImpl.getClassificationId()); + !Objects.equals(newTaskImpl.getClassificationId(), oldTaskImpl.getClassificationId()); final boolean isManualPriorityChanged = newTaskImpl.getManualPriority() != oldTaskImpl.getManualPriority(); diff --git a/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java b/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java index f0dc61556d..11908a091e 100644 --- a/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/AbstractAccTest.java @@ -1,6 +1,7 @@ package acceptance; import java.lang.reflect.Field; +import java.sql.SQLException; import java.time.Duration; import java.time.Instant; import java.time.LocalDate; @@ -56,6 +57,14 @@ protected static void destroyClass() { .ifPresent(JobScheduler::stop); } + protected static void initTaskanaEngine(TaskanaConfiguration configuration) throws SQLException { + taskanaConfiguration = configuration; + taskanaEngine = + TaskanaEngine.buildTaskanaEngine(taskanaConfiguration, ConnectionManagementMode.AUTOCOMMIT); + taskService = (TaskServiceImpl) taskanaEngine.getTaskService(); + workingTimeCalculator = taskanaEngine.getWorkingTimeCalculator(); + } + protected static void resetDb(boolean dropTables) throws Exception { DataSource dataSource = DataSourceGenerator.getDataSource(); diff --git a/lib/taskana-core/src/test/java/acceptance/persistence/UpdateObjectsUseUtcTimeStampsWithWorkingDaysCalculationAccTest.java b/lib/taskana-core/src/test/java/acceptance/persistence/UpdateObjectsUseUtcTimeStampsWithWorkingDaysCalculationAccTest.java new file mode 100644 index 0000000000..fcee9ff95a --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/persistence/UpdateObjectsUseUtcTimeStampsWithWorkingDaysCalculationAccTest.java @@ -0,0 +1,223 @@ +package acceptance.persistence; + +import static org.assertj.core.api.Assertions.assertThat; +import static pro.taskana.common.api.SharedConstants.MASTER_DOMAIN; + +import acceptance.AbstractAccTest; +import java.time.Duration; +import java.time.Instant; +import java.util.List; +import java.util.Map; +import java.util.TimeZone; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import pro.taskana.TaskanaConfiguration; +import pro.taskana.TaskanaConfiguration.Builder; +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.common.api.JobService; +import pro.taskana.common.api.ScheduledJob; +import pro.taskana.common.internal.JobServiceImpl; +import pro.taskana.common.test.security.JaasExtension; +import pro.taskana.common.test.security.WithAccessId; +import pro.taskana.task.api.TaskCustomField; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.models.Task; +import pro.taskana.task.internal.jobs.TaskCleanupJob; +import pro.taskana.task.internal.models.TaskImpl; +import pro.taskana.workbasket.api.WorkbasketCustomField; +import pro.taskana.workbasket.api.WorkbasketPermission; +import pro.taskana.workbasket.api.WorkbasketService; +import pro.taskana.workbasket.api.WorkbasketType; +import pro.taskana.workbasket.api.models.Workbasket; +import pro.taskana.workbasket.api.models.WorkbasketAccessItem; + +@ExtendWith(JaasExtension.class) +public class UpdateObjectsUseUtcTimeStampsWithWorkingDaysCalculationAccTest + extends AbstractAccTest { + + @BeforeAll + protected static void setupTest() throws Exception { + resetDb(false); + + TaskanaConfiguration config = + new Builder(taskanaConfiguration).useWorkingTimeCalculation(false).build(); + + initTaskanaEngine(config); + } + + @WithAccessId(user = "admin") + @Test + void testTimestampsOnTaskUpdate() throws Exception { + TaskService taskService = taskanaEngine.getTaskService(); + Task task = taskService.getTask("TKI:000000000000000000000000000000000000"); + Instant now = Instant.now(); + + task.setPlanned(now.plus(Duration.ofHours(17))); + + // associated Classification has ServiceLevel 'P1D' + task.setDue(workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1))); + + TaskImpl ti = (TaskImpl) task; + ti.setCompleted(now.plus(Duration.ofHours(27))); + TimeZone originalZone = TimeZone.getDefault(); + Task updatedTask = taskService.updateTask(task); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + Task retrievedTask = taskService.getTask(updatedTask.getId()); + TimeZone.setDefault(originalZone); + assertThat(retrievedTask).isEqualTo(updatedTask); + } + + @WithAccessId(user = "user-1-1") + @Test + void testCreatedTaskObjectEqualsReadTaskObjectInNewTimezone() throws Exception { + + TaskService taskService = taskanaEngine.getTaskService(); + Task newTask = taskService.newTask("USER-1-1", "DOMAIN_A"); + newTask.setClassificationKey("T2100"); + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + for (TaskCustomField taskCustomField : TaskCustomField.values()) { + newTask.setCustomField(taskCustomField, taskCustomField.name()); + } + newTask.setCustomAttributeMap(createSimpleCustomPropertyMap(5)); + newTask.setDescription("Description of test task"); + newTask.setNote("My note"); + newTask.addAttachment( + createExampleAttachment( + "DOCTYPE_DEFAULT", + createObjectReference( + "COMPANY_A", + "SYSTEM_B", + "INSTANCE_B", + "ArchiveId", + "12345678901234567890123456789012345678901234567890"), + "E-MAIL", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(3))); + + TimeZone originalZone = TimeZone.getDefault(); + Task createdTask = taskService.createTask(newTask); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + Task readTask = taskService.getTask(createdTask.getId()); + TimeZone.setDefault(originalZone); + assertThat(readTask).isEqualTo(createdTask); + } + + @WithAccessId(user = "admin") + @Test + void testTimestampsOnClassificationUpdate() throws Exception { + ClassificationService classificationService = taskanaEngine.getClassificationService(); + Classification classification = + classificationService.getClassification("CLI:000000000000000000000000000000000001"); + classification.setPriority(17); + + TimeZone originalZone = TimeZone.getDefault(); + Classification updatedClassification = + classificationService.updateClassification(classification); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + Classification retrievedClassification = + classificationService.getClassification(updatedClassification.getId()); + TimeZone.setDefault(originalZone); + assertThat(retrievedClassification).isEqualTo(updatedClassification); + } + + @WithAccessId(user = "businessadmin") + @Test + void testTimestampsOnCreateMasterClassification() throws Exception { + ClassificationService classificationService = taskanaEngine.getClassificationService(); + final long amountOfClassificationsBefore = + classificationService.createClassificationQuery().count(); + Classification classification = + classificationService.newClassification("Key0", MASTER_DOMAIN, "TASK"); + classification.setIsValidInDomain(true); + classification.setServiceLevel("P1D"); + classification = classificationService.createClassification(classification); + + // check only 1 created + long amountOfClassificationsAfter = classificationService.createClassificationQuery().count(); + assertThat(amountOfClassificationsAfter).isEqualTo(amountOfClassificationsBefore + 1); + + TimeZone originalZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + Classification retrievedClassification = + classificationService.getClassification(classification.getId()); + TimeZone.setDefault(originalZone); + + assertThat(retrievedClassification).isEqualTo(classification); + } + + @WithAccessId(user = "admin") + @Test + void testTimestampsOnWorkbasketUpdate() throws Exception { + WorkbasketService workbasketService = taskanaEngine.getWorkbasketService(); + Workbasket workbasket = + workbasketService.getWorkbasket("WBI:100000000000000000000000000000000001"); + workbasket.setCustomField(WorkbasketCustomField.CUSTOM_1, "bla"); + + TimeZone originalZone = TimeZone.getDefault(); + Workbasket updatedWorkbasket = workbasketService.updateWorkbasket(workbasket); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + Workbasket retrievedWorkbasket = workbasketService.getWorkbasket(updatedWorkbasket.getId()); + TimeZone.setDefault(originalZone); + assertThat(retrievedWorkbasket).isEqualTo(updatedWorkbasket); + } + + @WithAccessId(user = "businessadmin") + @Test + void testTimestampsOnCreateWorkbasket() throws Exception { + WorkbasketService workbasketService = taskanaEngine.getWorkbasketService(); + final int before = workbasketService.createWorkbasketQuery().domainIn("DOMAIN_A").list().size(); + + Workbasket workbasket = workbasketService.newWorkbasket("NT1234", "DOMAIN_A"); + workbasket.setName("Megabasket"); + workbasket.setType(WorkbasketType.GROUP); + workbasket.setOrgLevel1("company"); + workbasket = workbasketService.createWorkbasket(workbasket); + WorkbasketAccessItem wbai = + workbasketService.newWorkbasketAccessItem(workbasket.getId(), "user-1-2"); + wbai.setPermission(WorkbasketPermission.READ, true); + workbasketService.createWorkbasketAccessItem(wbai); + + int after = workbasketService.createWorkbasketQuery().domainIn("DOMAIN_A").list().size(); + assertThat(after).isEqualTo(before + 1); + + TimeZone originalZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + Workbasket retrievedWorkbasket = workbasketService.getWorkbasket("NT1234", "DOMAIN_A"); + TimeZone.setDefault(originalZone); + assertThat(retrievedWorkbasket).isEqualTo(workbasket); + } + + @WithAccessId(user = "user-1-2") + @Test + void testTimestampsOnCreateScheduledJob() throws Exception { + resetDb(true); + ScheduledJob job = new ScheduledJob(); + job.setArguments(Map.of("keyBla", "valueBla")); + job.setType(TaskCleanupJob.class.getName()); + job.setDue(Instant.now().minus(Duration.ofHours(5))); + job.setLockExpires(Instant.now().minus(Duration.ofHours(5))); + JobService jobService = taskanaEngine.getJobService(); + job = jobService.createJob(job); + TimeZone originalZone = TimeZone.getDefault(); + TimeZone.setDefault(TimeZone.getTimeZone("EST")); + + JobServiceImpl jobServiceImpl = (JobServiceImpl) jobService; + List jobs = jobServiceImpl.findJobsToRun(); + final ScheduledJob jobForLambda = job; + ScheduledJob retrievedJob = + jobs.stream() + .filter( + j -> + j.getJobId().equals(jobForLambda.getJobId()) + && j.getArguments() != null + && "valueBla".equals(j.getArguments().get("keyBla"))) + .findFirst() + .orElse(null); + + TimeZone.setDefault(originalZone); + assertThat(retrievedJob).isEqualTo(job); + } +} diff --git a/lib/taskana-core/src/test/java/acceptance/task/ServiceLevelPriorityWithWorkingDaysCalculationAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/ServiceLevelPriorityWithWorkingDaysCalculationAccTest.java new file mode 100644 index 0000000000..454100bb76 --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/task/ServiceLevelPriorityWithWorkingDaysCalculationAccTest.java @@ -0,0 +1,616 @@ +package acceptance.task; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import acceptance.AbstractAccTest; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.List; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.assertj.core.data.TemporalUnitWithinOffset; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import pro.taskana.TaskanaConfiguration; +import pro.taskana.TaskanaConfiguration.Builder; +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.common.api.BulkOperationResults; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.api.exceptions.TaskanaException; +import pro.taskana.common.test.security.JaasExtension; +import pro.taskana.common.test.security.WithAccessId; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.exceptions.TaskNotFoundException; +import pro.taskana.task.api.models.Task; +import pro.taskana.workbasket.api.exceptions.NotAuthorizedOnWorkbasketException; + +@ExtendWith(JaasExtension.class) +public class ServiceLevelPriorityWithWorkingDaysCalculationAccTest extends AbstractAccTest { + + private static ClassificationService classificationService; + + @BeforeAll + protected static void setupTest() throws Exception { + resetDb(false); + + TaskanaConfiguration config = + new Builder(taskanaConfiguration).useWorkingTimeCalculation(false).build(); + + initTaskanaEngine(config); + classificationService = taskanaEngine.getClassificationService(); + } + + /* CREATE TASK */ + + @WithAccessId(user = "user-1-1") + @Test + void should_CalculatePlannedDateAtCreate() throws Exception { + + // P16D + Classification classification = classificationService.getClassification("L110105", "DOMAIN_A"); + long serviceLevelDays = Duration.parse(classification.getServiceLevel()).toDays(); + assertThat(serviceLevelDays).isEqualTo(16); + + Task newTask = taskService.newTask("USER-1-1", classification.getDomain()); + newTask.setClassificationKey(classification.getKey()); + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + + Instant due = + moveBackToWorkingDay( + Instant.now().truncatedTo(ChronoUnit.MILLIS).plus(40, ChronoUnit.DAYS)); + newTask.setDue(due); + Task createdTask = taskService.createTask(newTask); + assertThat(createdTask.getId()).isNotNull(); + + Task readTask = taskService.getTask(createdTask.getId()); + assertThat(readTask).isNotNull(); + assertThat(readTask.getDue()).isEqualTo(due); + + Instant expectedPlanned = + workingTimeCalculator.subtractWorkingTime(due, Duration.ofDays(serviceLevelDays)); + assertThat(readTask.getPlanned()).isEqualTo(expectedPlanned); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_CalculateDueDateAtCreate() throws Exception { + + // P16D + Classification classification = classificationService.getClassification("L110105", "DOMAIN_A"); + long serviceLevelDays = Duration.parse(classification.getServiceLevel()).toDays(); + assertThat(serviceLevelDays).isEqualTo(16); + + Task newTask = taskService.newTask("USER-1-1", classification.getDomain()); + newTask.setClassificationKey(classification.getKey()); + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + + Instant planned = moveForwardToWorkingDay(Instant.now().truncatedTo(ChronoUnit.MILLIS)); + newTask.setPlanned(planned); + Task createdTask = taskService.createTask(newTask); + assertThat(createdTask.getId()).isNotNull(); + + Task readTask = taskService.getTask(createdTask.getId()); + assertThat(readTask).isNotNull(); + assertThat(readTask.getPlanned()).isEqualTo(planned); + + Instant expectedDue = + workingTimeCalculator.addWorkingTime( + readTask.getPlanned(), Duration.ofDays(serviceLevelDays)); + + assertThat(readTask.getDue()).isEqualTo(expectedDue); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_NotThrowException_When_DueAndPlannedAreConsistent() throws Exception { + + Classification classification = classificationService.getClassification("T2100", "DOMAIN_A"); + long duration = Duration.parse(classification.getServiceLevel()).toDays(); + + Task newTask = taskService.newTask("USER-1-1", "DOMAIN_A"); + newTask.setPlanned(moveForwardToWorkingDay(Instant.now())); + newTask.setClassificationKey(classification.getKey()); + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + newTask.setOwner("user-1-1"); + + // due date according to service level + Instant expectedDue = + workingTimeCalculator.addWorkingTime(newTask.getPlanned(), Duration.ofDays(duration)); + + newTask.setDue(expectedDue); + ThrowingCallable call = () -> taskService.createTask(newTask); + assertThatCode(call).doesNotThrowAnyException(); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_DueAndPlannedAreInconsistent() { + + Task newTask = taskService.newTask("USER-1-1", "DOMAIN_A"); + Instant planned = moveForwardToWorkingDay(Instant.now().plus(2, ChronoUnit.HOURS)); + newTask.setClassificationKey("T2100"); // P10D + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + newTask.setOwner("user-1-1"); + + newTask.setPlanned(planned); + newTask.setDue(planned); // due date not according to service level + ThrowingCallable call = () -> taskService.createTask(newTask); + assertThatThrownBy(call).isInstanceOf(InvalidArgumentException.class); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_VerifyThatCreateAndPlannedAreClose() throws Exception { + Instant now = Instant.now().truncatedTo(ChronoUnit.MILLIS); + Instant inTwoHours = now.plus(2, ChronoUnit.HOURS); + + Task newTask = taskService.newTask("USER-1-1", "DOMAIN_A"); + Instant planned = moveForwardToWorkingDay(inTwoHours); + newTask.setClassificationKey("T2100"); + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + newTask.setOwner("user-1-1"); + newTask.setPlanned(planned); + Task createdTask = taskService.createTask(newTask); + + assertThat(createdTask).isNotNull(); + assertThat(createdTask.getPlanned()).isEqualTo(planned); + assertThat(createdTask.getCreated()).isBefore(createdTask.getPlanned()); + + assertThat(createdTask.getPlanned()) + .isCloseTo( + moveForwardToWorkingDay(createdTask.getCreated()), + new TemporalUnitWithinOffset(2L, ChronoUnit.HOURS)); + } + + /* UPDATE TASK */ + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_DueAndPlannedAreChangedInconsistently() throws Exception { + TaskService taskService = taskanaEngine.getTaskService(); + Task task = taskService.getTask("TKI:000000000000000000000000000000000000"); // P1D + task.setDue(Instant.parse("2020-07-02T00:00:00Z")); + task.setPlanned(Instant.parse("2020-07-07T00:00:00Z")); + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessage( + "Cannot update a task with given planned 2020-07-07T00:00:00Z and due " + + "date 2020-07-02T00:00:00Z not matching the service level PT24H."); + } + + @WithAccessId(user = "user-b-2") + @Test + void should_SetPlanned_When_SetPlannedRequestContainsDuplicateTaskIds() throws Exception { + + // This test works with the following tasks (w/o attachments) and classifications + // + // +-----------------------------------------+------------------------------------------+------+ + // | TaskId | ClassificationId | SL | + // +-----------------------------------------+------------------------------------------+------+ + // |TKI:000000000000000000000000000000000058 | CLI:200000000000000000000000000000000017 | P1D | + // |TKI:000000000000000000000000000000000059 | CLI:200000000000000000000000000000000017 | P1D | + // |TKI:000000000000000000000000000000000060 | CLI:200000000000000000000000000000000017 | P1D | + // +-----------------------------------------+------------------------------------------+------+ + String tkId1 = "TKI:000000000000000000000000000000000058"; + String tkId2 = "TKI:000000000000000000000000000000000059"; + String tkId3 = "TKI:000000000000000000000000000000000058"; + String tkId4 = "TKI:000000000000000000000000000000000060"; + + List taskIds = List.of(tkId1, tkId2, tkId3, tkId4); + + Instant planned = getInstant("2020-02-11T07:00:00"); + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + assertThat(results.containsErrors()).isFalse(); + Instant dueExpected = getInstant("2020-02-12T07:00:00"); + + Instant due1 = taskService.getTask(tkId1).getDue(); + assertThat(due1).isEqualTo(dueExpected); + Instant due2 = taskService.getTask(tkId2).getDue(); + assertThat(due2).isEqualTo(dueExpected); + Instant due3 = taskService.getTask(tkId3).getDue(); + assertThat(due3).isEqualTo(dueExpected); + Instant due4 = taskService.getTask(tkId4).getDue(); + assertThat(due4).isEqualTo(dueExpected); + } + + @WithAccessId(user = "user-b-2") + @Test + void should_SetPlanned_When_RequestContainsDuplicatesAndNotExistingTaskIds() throws Exception { + + String tkId1 = "TKI:000000000000000000000000000000000058"; + String tkId2 = "TKI:000000000000000000000000000047110059"; + String tkId3 = "TKI:000000000000000000000000000000000059"; + String tkId4 = "TKI:000000000000000000000000000000000058"; + String tkId5 = "TKI:000000000000000000000000000000000060"; + List taskIds = List.of(tkId1, tkId2, tkId3, tkId4, tkId5); + Instant planned = getInstant("2020-04-20T07:00:00"); + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + assertThat(results.containsErrors()).isTrue(); + assertThat(results.getErrorMap()).hasSize(1); + assertThat(results.getErrorForId("TKI:000000000000000000000000000047110059")) + .isInstanceOf(TaskNotFoundException.class); + Instant dueExpected = getInstant("2020-04-21T07:00:00"); + Instant due1 = taskService.getTask(tkId1).getDue(); + assertThat(due1).isEqualTo(dueExpected); + Instant due3 = taskService.getTask(tkId3).getDue(); + assertThat(due3).isEqualTo(dueExpected); + Instant due5 = taskService.getTask(tkId5).getDue(); + assertThat(due5).isEqualTo(dueExpected); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_SetPlanned_When_RequestContainsTasksWithAttachments() throws Exception { + + // This test works with the following tasks, attachments and classifications + // + // +-----------------------------------------+------------------------------------------+------+ + // | Task / associated attachment | Classification | SL | + // +-----------------------------------------+------------------------------------------+------+ + // |TKI:000000000000000000000000000000000000 | CLI:100000000000000000000000000000000016 | P1D | + // |TAI:000000000000000000000000000000000000 | CLI:100000000000000000000000000000000003 | P13D | + // |TAI:000000000000000000000000000000000009 | CLI:100000000000000000000000000000000003 | P13D | + // +-----------------------------------------+------------------------------------------+------+ + // |TKI:000000000000000000000000000000000001 | CLI:100000000000000000000000000000000005 | P15D | + // |TAI:000000000000000000000000000000000001 | CLI:100000000000000000000000000000000002 | P2D | + // |TAI:000000000000000000000000000000000002 | CLI:000000000000000000000000000000000003 | P3D | + // +-----------------------------------------+------------------------------------------+------+ + // |TKI:000000000000000000000000000000000002 | CLI:100000000000000000000000000000000016 | P1D | + // |TAI:000000000000000000000000000000000003 | CLI:000000000000000000000000000000000004 | P4D | + // |TAI:000000000000000000000000000000000004 | CLI:000000000000000000000000000000000005 | P5D | + // |TAI:000000000000000000000000000000000005 | CLI:000000000000000000000000000000000006 | P5D | + // |TAI:000000000000000000000000000000000006 | CLI:000000000000000000000000000000000007 | P6D | + // |TAI:000000000000000000000000000000000007 | CLI:100000000000000000000000000000000008 | P1D | + // +-----------------------------------------+------------------------------------------+------+ + + String tkId0 = "TKI:000000000000000000000000000000000000"; + String tkId1 = "TKI:000000000000000000000000000000000001"; + String tkId2 = "TKI:000000000000000000000000000000000002"; + + // get due dates by updating the tasks individually + Task task0 = taskService.getTask(tkId0); + Task task1 = taskService.getTask(tkId1); + Task task2 = taskService.getTask(tkId2); + + Instant planned = getInstant("2020-04-21T13:00:00"); + task0.setPlanned(planned); + task1.setPlanned(planned); + task2.setPlanned(planned); + + final Instant due0 = taskService.updateTask(task0).getDue(); + final Instant due1 = taskService.updateTask(task1).getDue(); + final Instant due2 = taskService.updateTask(task2).getDue(); + + // now check that bulk update gives the same due dates + + List taskIds = List.of(tkId0, tkId1, tkId2); + + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + Instant dueBulk0 = taskService.getTask(tkId0).getDue(); + Instant dueBulk1 = taskService.getTask(tkId1).getDue(); + Instant dueBulk2 = taskService.getTask(tkId2).getDue(); + + assertThat(dueBulk0).isEqualTo(planned.plus(1, ChronoUnit.DAYS)); + assertThat(dueBulk1).isEqualTo(planned.plus(2, ChronoUnit.DAYS)); + assertThat(dueBulk2).isEqualTo(planned.plus(1, ChronoUnit.DAYS)); + + assertThat(results.containsErrors()).isFalse(); + assertThat(dueBulk0).isEqualTo(due0); + assertThat(dueBulk1).isEqualTo(due1); + assertThat(dueBulk2).isEqualTo(due2); + } + + // the following tests use these tasks, attachments and classifications + // +-----------------------------------------+------------------------------------------+------+ + // | Task / associated attachment | Classification | SL | + // +-----------------------------------------+------------------------------------------+------+ + // |TKI:000000000000000000000000000000000008 | CLI:100000000000000000000000000000000003 | P13D | + // |TAI:000000000000000000000000000000000008 | CLI:000000000000000000000000000000000009 | P8D | + // +---------------------------------------- + -----------------------------------------+ -----+ + // |TKI:000000000000000000000000000000000009 | CLI:100000000000000000000000000000000003 | P13D | + // +---------------------------------------- + -----------------------------------------+ -----+ + // |TKI:000000000000000000000000000000000010 | CLI:100000000000000000000000000000000003 | P13D | + // |TAI:000000000000000000000000000000000014 | CLI:100000000000000000000000000000000004,| P14D | + // +-----------------------------------------+------------------------------------------+------+ + + @WithAccessId(user = "user-b-2") + @Test + void should_ReturnBulkLog_When_UserIsNotAuthorizedForTasks() { + String tkId1 = "TKI:000000000000000000000000000000000008"; + String tkId2 = "TKI:000000000000000000000000000000000009"; + String tkId3 = "TKI:000000000000000000000000000000000008"; + String tkId4 = "TKI:000000000000000000000000000000000010"; + List taskIds = List.of(tkId1, tkId2, tkId3, tkId4); + Instant planned = getInstant("2020-02-25T07:00:00"); + + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + + assertThat(results.containsErrors()).isTrue(); + assertThat(results.getFailedIds()).hasSize(3).containsAnyElementsOf(taskIds); + assertThat(results.getErrorMap().values()) + .hasOnlyElementsOfType(NotAuthorizedOnWorkbasketException.class); + } + + @WithAccessId(user = "admin") + @Test + void should_SetPlannedPropertyOfTasks_When_RequestedByAdminUser() throws Exception { + String tkId1 = "TKI:000000000000000000000000000000000008"; + String tkId2 = "TKI:000000000000000000000000000000000009"; + String tkId3 = "TKI:000000000000000000000000000000000008"; + String tkId4 = "TKI:000000000000000000000000000000000010"; // all three have P13D + + List taskIds = List.of(tkId1, tkId2, tkId3, tkId4); + Instant planned = getInstant("2020-05-03T07:00:00"); + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, taskIds); + assertThat(results.containsErrors()).isFalse(); + + Instant dueBulk1 = taskService.getTask(tkId1).getDue(); + Instant dueBulk2 = taskService.getTask(tkId2).getDue(); + Instant dueBulk3 = taskService.getTask(tkId3).getDue(); + Instant dueBulk4 = taskService.getTask(tkId4).getDue(); + assertThat(dueBulk1).isEqualTo(getInstant("2020-05-14T07:00:00")); + assertThat(dueBulk2).isEqualTo(getInstant("2020-05-22T07:00:00")); + assertThat(dueBulk3).isEqualTo(getInstant("2020-05-14T07:00:00")); + assertThat(dueBulk4).isEqualTo(getInstant("2020-05-22T07:00:00")); + } + + @WithAccessId(user = "admin") + @Test + void should_DoNothing_When_SetPlannedIsCalledWithEmptyTasksList() { + Instant planned = getInstant("2020-05-03T07:00:00"); + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, new ArrayList<>()); + assertThat(results.containsErrors()).isFalse(); + } + + // +-----------------------------------------+------------------------------------------+------+ + // |TKI:000000000000000000000000000000000002 | CLI:100000000000000000000000000000000016 | P1D | + // |TAI:000000000000000000000000000000000003 | CLI:000000000000000000000000000000000004 | P4D | + // |TAI:000000000000000000000000000000000004 | CLI:000000000000000000000000000000000005 | P5D | + // |TAI:000000000000000000000000000000000005 | CLI:000000000000000000000000000000000006 | P5D | + // |TAI:000000000000000000000000000000000006 | CLI:000000000000000000000000000000000007 | P6D | + // |TAI:000000000000000000000000000000000007 | CLI:100000000000000000000000000000000008 | P1D | + // |TKI:000000000000000000000000000000000066 | CLI:100000000000000000000000000000000024 | P0D | + // +-----------------------------------------+------------------------------------------+------+ + @WithAccessId(user = "admin") + @Test + void should_SetPlannedPropertyWithBulkUpdate_When_RequestContainsASingleTask() throws Exception { + String taskId = "TKI:000000000000000000000000000000000002"; + Instant planned = getInstant("2020-05-03T07:00:00"); + // test bulk operation setPlanned... + BulkOperationResults results = + taskService.setPlannedPropertyOfTasks(planned, List.of(taskId)); + Task task = taskService.getTask(taskId); + assertThat(results.containsErrors()).isFalse(); + Instant expectedDue = + workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expectedDue); + } + + @WithAccessId(user = "admin") + @Test + void should_SetPlannedPropertyOnSingle_When_UpdateTaskWasCalled() throws Exception { + String taskId = "TKI:000000000000000000000000000000000002"; + Task task = taskService.getTask(taskId); + // test update of planned date via updateTask() + task.setPlanned(task.getPlanned().plus(Duration.ofDays(3))); + task = taskService.updateTask(task); + Instant expectedDue = + workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expectedDue); + } + + @WithAccessId(user = "admin") + @Test + void should_SetPlanned_When_OnlyDueWasChanged() throws Exception { + String taskId = "TKI:000000000000000000000000000000000002"; // P1D + Instant planned = getInstant("2020-05-03T07:00:00"); + Task task = taskService.getTask(taskId); + + // test update of due with unchanged planned + task.setDue(planned.plus(Duration.ofDays(8))); // Monday + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-05-08T07:00:00")); // Friday + } + + @WithAccessId(user = "admin") + @Test + void should_SetDue_When_OnlyPlannedWasChanged() throws Exception { + String taskId = "TKI:000000000000000000000000000000000002"; // P1D ServiceLevel + Task task = taskService.getTask(taskId); + Instant planned = getInstant("2020-05-10T07:00:00"); // Sunday + task.setPlanned(planned); + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-05-11T07:00:00")); // Monday + assertThat(task.getDue()).isEqualTo(getInstant("2020-05-12T07:00:00")); // Tuesday + } + + @WithAccessId(user = "admin") + @Test + void should_SetPlanned_When_DueIsChangedAndPlannedIsNulled() throws Exception { + String taskId = "TKI:000000000000000000000000000000000002"; + Instant due = getInstant("2020-05-06T07:00:00"); + Task task = taskService.getTask(taskId); + task.setDue(due); + task.setPlanned(null); + task = taskService.updateTask(task); + + String serviceLevel = task.getClassificationSummary().getServiceLevel(); + Instant expPlanned = + workingTimeCalculator.subtractWorkingTime(task.getDue(), Duration.parse(serviceLevel)); + assertThat(task.getPlanned()).isEqualTo(expPlanned); + assertThat(task.getDue()).isEqualTo(due); + } + + @WithAccessId(user = "admin") + @Test + void should_SetDue_When_TaskUpdateIsCalled() throws Exception { + String taskId = "TKI:000000000000000000000000000000000002"; + final Instant planned = getInstant("2020-05-03T07:00:00"); // Sunday + Task task = taskService.getTask(taskId); + + task.setPlanned(null); + task = taskService.updateTask(task); + Instant expectedDue = + workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expectedDue); + + task.setDue(null); + task = taskService.updateTask(task); + expectedDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expectedDue); + + task.setPlanned(planned.plus(Duration.ofDays(13))); // Saturday + task.setDue(null); + task = taskService.updateTask(task); + expectedDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expectedDue); + + task.setDue(planned.plus(Duration.ofDays(13))); // Saturday + task.setPlanned(null); + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-05-14T07:00:00")); + assertThat(task.getDue()).isEqualTo(getInstant("2020-05-15T07:00:00")); + } + + @WithAccessId(user = "user-1-2") + @Test + void should_UpdateTaskPlannedOrDue_When_PlannedOrDueAreWeekendDays() throws Exception { + Task task = taskService.getTask("TKI:000000000000000000000000000000000030"); // SL=P13D + task.setPlanned(getInstant("2020-03-21T07:00:00")); // planned = saturday + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-04-09T07:00:00")); + + task.setDue(getInstant("2020-04-11T07:00:00")); // due = saturday + task.setPlanned(null); + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); + + task.setDue(getInstant("2020-04-12T07:00:00")); // due = sunday + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); + + task.setPlanned(getInstant("2020-03-21T07:00:00")); // planned = saturday + task.setDue(getInstant("2020-04-09T07:00:00")); // thursday + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); + + task.setPlanned(getInstant("2020-03-03T07:00:00")); // planned on tuesday + task.setDue(getInstant("2020-03-22T07:00:00")); // due = sunday + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-20T07:00:00")); // friday + + task.setPlanned(getInstant("2024-03-29T07:00:00")); // Good Friday + task.setDue(null); + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2024-04-02T07:00:00")); // Tuesday + assertThat(task.getDue()).isEqualTo(getInstant("2024-04-19T07:00:00")); // Friday + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UpdateTaskPlannedAndDueDate_When_PlannedDateIsNotWorkingDay() throws Exception { + Task task = taskService.getTask("TKI:000000000000000000000000000000000201"); // SL=P2D + assertThat(task.getClassificationSummary().getServiceLevel()).isEqualTo("P2D"); + task.setPlanned(getInstant("2024-03-29T07:00:00")); // planned = Good Friday + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2024-04-02T07:00:00")); // Tuesday + assertThat(task.getDue()).isEqualTo(getInstant("2024-04-04T07:00:00")); // Thursday + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UpdateTaskPlannedOrDue_When_PlannedOrDueAreOnWeekends_ServiceLevel_P0D() + throws Exception { + Task task = taskService.getTask("TKI:000000000000000000000000000000000066"); // P0D + + // nothing changed + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2018-01-29T15:55:00")); // Monday + assertThat(task.getPlanned()).isEqualTo(getInstant("2018-01-29T15:55:00")); // Monday + + // planned changed, due did not change + task.setPlanned(getInstant("2020-03-21T07:00:00")); // Saturday + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + + // due changed, planned did not change + task.setDue(getInstant("2020-04-12T07:00:00")); // Sunday + task = taskService.updateTask(task); + assertThat(task.getPlanned()) + .isEqualTo(getInstant("2020-04-09T07:00:00")); // Thursday (skip Good Friday) + assertThat(task.getDue()).isEqualTo(getInstant("2020-04-09T07:00:00")); + + // due changed, planned is null + task.setDue(getInstant("2020-04-11T07:00:00")); // Saturday + task.setPlanned(null); + task = taskService.updateTask(task); + assertThat(task.getPlanned()) + .isEqualTo(getInstant("2020-04-09T07:00:00")); // Thursday (skip Good Friday) + assertThat(task.getDue()).isEqualTo(getInstant("2020-04-09T07:00:00")); + + // planned changed, due is null + task.setPlanned(getInstant("2020-03-22T07:00:00")); // Sunday + task.setDue(null); + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + + // both changed, not null (due at weekend) + task.setPlanned(getInstant("2020-03-20T07:00:00")); // Friday + task.setDue(getInstant("2020-03-22T07:00:00")); // Sunday + task = taskService.updateTask(task); + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday + + // both changed, not null (planned at weekend) + task.setPlanned(getInstant("2020-03-22T07:00:00")); // Sunday + task.setDue(getInstant("2020-03-23T07:00:00")); // Monday + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + + // both changed, not null (both at weekend) within SLA + task.setPlanned(getInstant("2020-03-22T07:00:00")); // Sunday + task.setDue(getInstant("2020-03-22T07:00:00")); // Sunday + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-20T07:00:00")); // Friday + + // both changed, not null (planned > due) + task.setPlanned(getInstant("2020-03-24T07:00:00")); // Tuesday + task.setDue(getInstant("2020-03-23T07:00:00")); // Monday + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-03-23T07:00:00")); // Monday + } + + @WithAccessId(user = "user-1-1") + @Test + void should_NotThrowServiceLevelViolation_IfWeekendOrHolidaysBetweenDates() throws Exception { + Task task = taskService.getTask("TKI:000000000000000000000000000000000002"); // P1D + + // SLA is broken but only with holidays in between + task.setDue(getInstant("2020-04-14T07:00:00")); // Tuesday after Easter + task.setPlanned(getInstant("2020-04-09T07:00:00")); // Thursday before Easter + task = taskService.updateTask(task); + assertThat(task.getDue()).isEqualTo(getInstant("2020-04-14T07:00:00")); // Tuesday + assertThat(task.getPlanned()).isEqualTo(getInstant("2020-04-09T07:00:00")); // Thursday + } +} diff --git a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksAccTest.java index bd9251ae0f..34ee60dd55 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksAccTest.java @@ -346,7 +346,7 @@ Stream should_ReturnCorrectResults_When_QueryingForCustomXStatement List> list = List.of( Triplet.of( - TaskCustomField.CUSTOM_1, new String[] {"custom%", "p%", "%xyz%", "efg"}, 3), + TaskCustomField.CUSTOM_1, new String[] {"custom%", "p%", "%xyz%", "efg"}, 4), Triplet.of(TaskCustomField.CUSTOM_2, new String[] {"custom%", "a%"}, 2), Triplet.of(TaskCustomField.CUSTOM_3, new String[] {"ffg"}, 1), Triplet.of(TaskCustomField.CUSTOM_4, new String[] {"%ust%", "%ty"}, 2), @@ -359,7 +359,7 @@ Stream should_ReturnCorrectResults_When_QueryingForCustomXStatement Triplet.of(TaskCustomField.CUSTOM_11, new String[] {"%ert"}, 3), Triplet.of(TaskCustomField.CUSTOM_12, new String[] {"dd%"}, 1), Triplet.of(TaskCustomField.CUSTOM_13, new String[] {"%dd_"}, 1), - Triplet.of(TaskCustomField.CUSTOM_14, new String[] {"%"}, 99), + Triplet.of(TaskCustomField.CUSTOM_14, new String[] {"%"}, 100), Triplet.of(TaskCustomField.CUSTOM_15, new String[] {"___"}, 4), Triplet.of(TaskCustomField.CUSTOM_16, new String[] {"___"}, 4)); assertThat(list).hasSameSizeAs(TaskCustomField.values()); @@ -391,22 +391,22 @@ Stream should_ReturnCorrectResults_When_QueryingForCustomXNotIn() { // carefully constructed to always return exactly 2 results List> list = List.of( - Triplet.of(TaskCustomField.CUSTOM_1, new String[] {"custom1"}, 98), + Triplet.of(TaskCustomField.CUSTOM_1, new String[] {"custom1"}, 99), Triplet.of(TaskCustomField.CUSTOM_2, new String[] {""}, 2), - Triplet.of(TaskCustomField.CUSTOM_3, new String[] {"custom3"}, 98), + Triplet.of(TaskCustomField.CUSTOM_3, new String[] {"custom3"}, 99), Triplet.of(TaskCustomField.CUSTOM_4, new String[] {""}, 2), - Triplet.of(TaskCustomField.CUSTOM_5, new String[] {"ew", "al", "el"}, 92), - Triplet.of(TaskCustomField.CUSTOM_6, new String[] {"11", "vvg"}, 95), - Triplet.of(TaskCustomField.CUSTOM_7, new String[] {"custom7", "ijk"}, 97), - Triplet.of(TaskCustomField.CUSTOM_8, new String[] {"not_existing"}, 99), - Triplet.of(TaskCustomField.CUSTOM_9, new String[] {"custom9"}, 98), - Triplet.of(TaskCustomField.CUSTOM_10, new String[] {"custom10"}, 98), - Triplet.of(TaskCustomField.CUSTOM_11, new String[] {"custom11"}, 98), - Triplet.of(TaskCustomField.CUSTOM_12, new String[] {"custom12"}, 98), - Triplet.of(TaskCustomField.CUSTOM_13, new String[] {"custom13"}, 98), + Triplet.of(TaskCustomField.CUSTOM_5, new String[] {"ew", "al", "el"}, 93), + Triplet.of(TaskCustomField.CUSTOM_6, new String[] {"11", "vvg"}, 96), + Triplet.of(TaskCustomField.CUSTOM_7, new String[] {"custom7", "ijk"}, 98), + Triplet.of(TaskCustomField.CUSTOM_8, new String[] {"not_existing"}, 100), + Triplet.of(TaskCustomField.CUSTOM_9, new String[] {"custom9"}, 99), + Triplet.of(TaskCustomField.CUSTOM_10, new String[] {"custom10"}, 99), + Triplet.of(TaskCustomField.CUSTOM_11, new String[] {"custom11"}, 99), + Triplet.of(TaskCustomField.CUSTOM_12, new String[] {"custom12"}, 99), + Triplet.of(TaskCustomField.CUSTOM_13, new String[] {"custom13"}, 99), Triplet.of(TaskCustomField.CUSTOM_14, new String[] {"abc"}, 0), - Triplet.of(TaskCustomField.CUSTOM_15, new String[] {"custom15"}, 98), - Triplet.of(TaskCustomField.CUSTOM_16, new String[] {"custom16"}, 98)); + Triplet.of(TaskCustomField.CUSTOM_15, new String[] {"custom15"}, 99), + Triplet.of(TaskCustomField.CUSTOM_16, new String[] {"custom16"}, 99)); assertThat(list).hasSameSizeAs(TaskCustomField.values()); return DynamicTest.stream( @@ -455,7 +455,7 @@ void should_ReturnTasksWithNullCustomField_When_QueriedByCustomFieldWhichIsEmpty throws InvalidArgumentException { List results = taskService.createTaskQuery().customAttributeIn(TaskCustomField.CUSTOM_9, "").list(); - assertThat(results).hasSize(97); + assertThat(results).hasSize(98); } @WithAccessId(user = "admin") @@ -467,7 +467,7 @@ void should_AllowToQueryTasksByCustomFieldWithNullAndEmptyInParallel() .createTaskQuery() .customAttributeIn(TaskCustomField.CUSTOM_9, "", null) .list(); - assertThat(results).hasSize(98); + assertThat(results).hasSize(99); } @WithAccessId(user = "admin") @@ -479,14 +479,14 @@ void should_ReturnTasksWithEmptyCustomField_When_QueriedByCustomFieldWhichIsNotN .createTaskQuery() .customAttributeNotIn(TaskCustomField.CUSTOM_9, new String[] {null}) .list(); - assertThat(results).hasSize(98); + assertThat(results).hasSize(99); results = taskService .createTaskQuery() .customAttributeNotIn(TaskCustomField.CUSTOM_9, null, "custom9") .list(); - assertThat(results).hasSize(97); + assertThat(results).hasSize(98); results = taskService @@ -494,7 +494,7 @@ void should_ReturnTasksWithEmptyCustomField_When_QueriedByCustomFieldWhichIsNotN .customAttributeNotIn(TaskCustomField.CUSTOM_9, new String[] {null}) .customAttributeNotIn(TaskCustomField.CUSTOM_10, "custom10") .list(); - assertThat(results).hasSize(97); + assertThat(results).hasSize(98); } @WithAccessId(user = "admin") diff --git a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByRoleAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByRoleAccTest.java index daae90a14b..efc4674519 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByRoleAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByRoleAccTest.java @@ -47,7 +47,7 @@ void should_FindAllAccessibleTasksDependentOnTheUser_When_MakingTaskQuery() { switch (taskanaEngine.getCurrentUserContext().getUserid()) { case "admin": case "taskadmin": - expectedSize = 99; + expectedSize = 100; break; case "businessadmin": case "monitor": @@ -57,7 +57,7 @@ void should_FindAllAccessibleTasksDependentOnTheUser_When_MakingTaskQuery() { expectedSize = 26; break; case "user-1-1": - expectedSize = 9; + expectedSize = 10; break; case "user-taskrouter": expectedSize = 0; diff --git a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByTimeIntervalsAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByTimeIntervalsAccTest.java index 6e027b70cc..ba538db0ca 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByTimeIntervalsAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksByTimeIntervalsAccTest.java @@ -92,7 +92,7 @@ void testCreatedAfter() { List results = taskService.createTaskQuery().createdWithin(interval1).orderByCreated(asc).list(); - assertThat(results).hasSize(61); + assertThat(results).hasSize(62); TaskSummary previousSummary = null; for (TaskSummary taskSummary : results) { Instant cr = taskSummary.getCreated(); diff --git a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksListValuesAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksListValuesAccTest.java index d6e95b214d..60a8425fe6 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksListValuesAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksListValuesAccTest.java @@ -62,7 +62,7 @@ void testQueryTaskValuesForClassificationName() { .ownerLike("%user%") .orderByClassificationName(ASCENDING) .listValues(TaskQueryColumnName.CLASSIFICATION_NAME, null); - assertThat(columnValueList).hasSize(5); + assertThat(columnValueList).hasSize(6); } @WithAccessId(user = "admin") diff --git a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksWithPaginationAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksWithPaginationAccTest.java index f01c412596..9c380cf2ff 100644 --- a/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksWithPaginationAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/task/query/QueryTasksWithPaginationAccTest.java @@ -30,9 +30,9 @@ class PaginationTest { void testQueryAllPaged() { TaskQuery taskQuery = taskanaEngine.getTaskService().createTaskQuery(); long numberOfTasks = taskQuery.count(); - assertThat(numberOfTasks).isEqualTo(99); + assertThat(numberOfTasks).isEqualTo(100); List tasks = taskQuery.orderByDue(DESCENDING).list(); - assertThat(tasks).hasSize(99); + assertThat(tasks).hasSize(100); List tasksp = taskQuery.orderByDue(DESCENDING).listPage(4, 5); assertThat(tasksp).hasSize(5); tasksp = taskQuery.orderByDue(DESCENDING).listPage(5, 5); diff --git a/lib/taskana-core/src/test/java/acceptance/task/update/UpdateTaskAttachmentWithWorkingDaysCalculationAccTest.java b/lib/taskana-core/src/test/java/acceptance/task/update/UpdateTaskAttachmentWithWorkingDaysCalculationAccTest.java new file mode 100644 index 0000000000..03e2846e82 --- /dev/null +++ b/lib/taskana-core/src/test/java/acceptance/task/update/UpdateTaskAttachmentWithWorkingDaysCalculationAccTest.java @@ -0,0 +1,655 @@ +package acceptance.task.update; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import acceptance.AbstractAccTest; +import java.time.Duration; +import java.time.Instant; +import java.util.ArrayList; +import org.assertj.core.api.Condition; +import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import pro.taskana.TaskanaConfiguration; +import pro.taskana.TaskanaConfiguration.Builder; +import pro.taskana.classification.api.ClassificationService; +import pro.taskana.classification.api.exceptions.ClassificationNotFoundException; +import pro.taskana.classification.api.models.Classification; +import pro.taskana.classification.api.models.ClassificationSummary; +import pro.taskana.classification.internal.models.ClassificationSummaryImpl; +import pro.taskana.common.api.exceptions.InvalidArgumentException; +import pro.taskana.common.test.security.JaasExtension; +import pro.taskana.common.test.security.WithAccessId; +import pro.taskana.task.api.TaskService; +import pro.taskana.task.api.exceptions.AttachmentPersistenceException; +import pro.taskana.task.api.models.Attachment; +import pro.taskana.task.api.models.AttachmentSummary; +import pro.taskana.task.api.models.Task; +import pro.taskana.task.api.models.TaskSummary; +import pro.taskana.task.internal.models.AttachmentImpl; +import pro.taskana.task.internal.models.TaskImpl; + +@ExtendWith(JaasExtension.class) +public class UpdateTaskAttachmentWithWorkingDaysCalculationAccTest extends AbstractAccTest { + + private Task task; + private Attachment attachment; + private ClassificationService classificationService; + + @BeforeEach + @WithAccessId(user = "admin") + void setUp() throws Exception { + resetDb(false); + + TaskanaConfiguration config = + new Builder(taskanaConfiguration).useWorkingTimeCalculation(false).build(); + + initTaskanaEngine(config); + classificationService = taskanaEngine.getClassificationService(); + task = + taskService.getTask( + "TKI:000000000000000000000000000000000000"); // class T2000, prio 1, SL P1D + task.setClassificationKey("T2000"); + attachment = + createExampleAttachment( + "DOCTYPE_DEFAULT", // prio 99, SL P2000D + createObjectReference( + "COMPANY_A", + "SYSTEM_B", + "INSTANCE_B", + "ArchiveId", + "12345678901234567890123456789012345678901234567890"), + "E-MAIL", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(3)); + task.getAttachments().clear(); + taskService.updateTask(task); + assertThat(task).isNotNull(); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UpdateTaskCorrectlyInDatabase_When_AddingAnAttachment() throws Exception { + final int attachmentCount = task.getAttachments().size(); + assertThat(task.getPriority()).isEqualTo(1); + assertThat(task.getPlanned().plus(Duration.ofDays(1))).isEqualTo(task.getDue()); + task.addAttachment(attachment); + + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + + assertThat(task.getAttachments()) + .hasSize(attachmentCount + 1) + .contains(attachment) + .extracting(Attachment::getModified) + .containsExactlyInAnyOrder(task.getModified()); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UpdateTaskReceived_When_AddingAnAttachment() throws Exception { + task.addAttachment(attachment); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task) + .extracting(TaskSummary::getReceived) + .isEqualTo(Instant.parse("2018-01-15T00:00:00Z")); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_NotUpdateTaskReceived_When_TaskAlreadyHasAReceived() throws Exception { + task.setReceived(Instant.parse("2019-09-13T08:44:17.588Z")); + task.addAttachment(attachment); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task).extracting(TaskSummary::getReceived).isNotEqualTo(attachment.getReceived()); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UpdateTaskReceived_When_AddingTwoAttachments() throws Exception { + task.addAttachment(attachment); + Attachment attachment2 = + createExampleAttachment( + "L10303", + createObjectReference( + "COMPANY_B", + "SYSTEM_C", + "INSTANCE_C", + "ArchiveId", + "ABC45678901234567890123456789012345678901234567890"), + "ROHRPOST", + Instant.parse("2018-01-12T00:00:00Z"), + createSimpleCustomPropertyMap(4)); + + task.addAttachment(attachment2); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task) + .extracting(TaskSummary::getReceived) + .isEqualTo(Instant.parse("2018-01-12T00:00:00Z")); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_NotAddSameAttachmentAgain_When_AddingToTaskSummary() throws Exception { + task.getAttachments().clear(); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).isEmpty(); + + AttachmentImpl attachment = (AttachmentImpl) this.attachment; + attachment.setId("TAI:000017"); + task.addAttachment(attachment); + task.addAttachment(attachment); + task = taskService.updateTask(task); + + assertThat(task.getAttachments()) + .hasSize(1) + .extracting(AttachmentSummary::getModified) + .containsExactlyInAnyOrder(task.getModified()); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowAttachmentPersistenceException_When_UpdatingTaskWithTwoIdenticalAttachments() + throws Exception { + final int attachmentCount = 0; + task.getAttachments().clear(); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount); + + AttachmentImpl attachment = (AttachmentImpl) this.attachment; + attachment.setId("TAI:000017"); + task.getAttachments().add(attachment); + task.getAttachments().add(attachment); + ThrowingCallable call = () -> taskService.updateTask(task); + assertThatThrownBy(call).isInstanceOf(AttachmentPersistenceException.class); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_UpdateExistingAttachment_When_AddingSameButNotEqualAttachmentAgain() + throws Exception { + // Add attachment before + task = taskService.getTask(task.getId()); + final int attachmentCount = task.getAttachments().size(); + task.addAttachment(attachment); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount + 1); + + // Change sth. and add same (id) again - override/update + String newChannel = "UPDATED EXTERNAL SINCE LAST ADD"; + final int attachmentCount2 = task.getAttachments().size(); + Attachment updatedAttachment = task.getAttachments().get(0); + updatedAttachment.setChannel(newChannel); + Classification newClassification = + taskanaEngine + .getClassificationService() + .getClassification("CLI:100000000000000000000000000000000013"); // Prio 99, P2000D + updatedAttachment.setClassificationSummary(newClassification.asSummary()); + task.addAttachment(updatedAttachment); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount2); + assertThat(task.getAttachments().get(0).getChannel()).isEqualTo(newChannel); + assertThat(task.getPriority()).isEqualTo(99); + + Instant expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expDue); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_NotUpdateExistingAttachment_When_AddingIdenticalAttachmentAgain() throws Exception { + // Add Attachment before + final int attachmentCount = task.getAttachments().size(); + ((AttachmentImpl) attachment).setId("TAI:0001"); + task.addAttachment(attachment); + task.addAttachment(attachment); // overwrite, same id + task.addAttachment(attachment); // overwrite, same id + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount + 1); + + // Add same again - ignored + final int attachmentCount2 = task.getAttachments().size(); + Attachment redundantAttachment = task.getAttachments().get(0); + task.addAttachment(redundantAttachment); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(attachmentCount2); + } + + @WithAccessId(user = "user-1-1") + @Test + void testAddAttachmentAsNullValueWillBeIgnored() throws Exception { + // Try to add a single NULL-Element + final int attachmentCount = task.getAttachments().size(); + task.addAttachment(null); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount); + + // Try to set the Attachments to NULL and update it + ((TaskImpl) task).setAttachments(null); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(attachmentCount); // locally, not inserted + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount); // inserted values not changed + + // Test no NullPointer on NULL-Value and removing it on current data. + // New loading can do this, but returned value should got this "function", too. + final int attachmentCount2 = task.getAttachments().size(); + task.getAttachments().add(null); + task.getAttachments().add(null); + task.getAttachments().add(null); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(attachmentCount2); // locally, not inserted + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount2); // inserted values not changed + assertThat(task.getPriority()).isEqualTo(1); + assertThat(task.getPlanned().plus(Duration.ofDays(1))).isEqualTo(task.getDue()); + } + + @WithAccessId(user = "user-1-1") + @Test + void testRemoveAttachment() throws Exception { + task.addAttachment(attachment); + task = taskService.updateTask(task); + assertThat(task.getPriority()).isEqualTo(99); + assertThat(task.getPlanned().plus(Duration.ofDays(1))).isEqualTo(task.getDue()); + int attachmentCount = task.getAttachments().size(); + Attachment attachmentToRemove = task.getAttachments().get(0); + task.removeAttachment(attachmentToRemove.getId()); + task = taskService.updateTask(task); + assertThat(task.getAttachments()) + .hasSize(attachmentCount - 1); // locally, removed and not inserted + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount - 1); // inserted, values removed + assertThat(task.getPriority()).isEqualTo(1); + assertThat(task.getPlanned().plus(Duration.ofDays(1))).isEqualTo(task.getDue()); + } + + @WithAccessId(user = "user-1-1") + @Test + void testRemoveAttachmentWithNullAndNotAddedId() throws Exception { + task.addAttachment(attachment); + task = taskService.updateTask(task); + int attachmentCount = task.getAttachments().size(); + + task.removeAttachment(null); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(attachmentCount); // locally, nothing changed + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount); // inserted, still same + + task.removeAttachment("INVALID ID HERE"); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(attachmentCount); // locally, nothing changed + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount); // inserted, still same + } + + @WithAccessId(user = "user-1-1") + @Test + void testUpdateAttachment() throws Exception { + ((TaskImpl) task).setAttachments(new ArrayList<>()); + task = taskService.updateTask(task); + assertThat(task.getPriority()).isEqualTo(1); + assertThat(task.getPlanned().plus(Duration.ofDays(1))).isEqualTo(task.getDue()); + + Attachment attachment = this.attachment; + task.addAttachment(attachment); + task = taskService.updateTask(task); + assertThat(task.getPriority()).isEqualTo(99); + assertThat(task.getPlanned().plus(Duration.ofDays(1))).isEqualTo(task.getDue()); + + final int attachmentCount = task.getAttachments().size(); + + String newChannel = attachment.getChannel() + "-X"; + task.getAttachments().get(0).setChannel(newChannel); + Classification newClassification = + taskanaEngine + .getClassificationService() + .getClassification("CLI:100000000000000000000000000000000013"); // Prio 99, P2000D + task.getAttachments().get(0).setClassificationSummary(newClassification.asSummary()); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(attachmentCount); + assertThat(task.getAttachments().get(0).getChannel()).isEqualTo(newChannel); + assertThat(task.getPriority()).isEqualTo(99); + Instant expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + + assertThat(task.getDue()).isEqualTo(expDue); + } + + @WithAccessId(user = "user-1-1") + @Test + void modifyExistingAttachment() throws Exception { + // setup test + assertThat(task.getAttachments()).isEmpty(); + task.addAttachment(attachment); + + Attachment attachment2 = + createExampleAttachment( + "L10303", // prio 101, SL PT7H + createObjectReference( + "COMPANY_B", + "SYSTEM_C", + "INSTANCE_C", + "ArchiveId", + "ABC45678901234567890123456789012345678901234567890"), + "ROHRPOST", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(4)); + task.addAttachment(attachment2); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + + assertThat(task.getPriority()).isEqualTo(101); + Instant expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(1)); + assertThat(task.getDue()).isEqualTo(expDue); + assertThat(task.getAttachments()) + .hasSize(2) + .areExactly( + 1, + new Condition<>( + e -> "E-MAIL".equals(e.getChannel()) && e.getCustomAttributeMap().size() == 3, + "E-MAIL with 3 custom attributes")) + .areExactly( + 1, + new Condition<>( + e -> "ROHRPOST".equals(e.getChannel()) && e.getCustomAttributeMap().size() == 4, + "ROHRPOST with 4 custom attributes")); + + ClassificationSummary newClassificationSummary = + taskanaEngine + .getClassificationService() + .getClassification("CLI:100000000000000000000000000000000006") // Prio 5, SL P16D + .asSummary(); + // modify existing attachment + for (Attachment att : task.getAttachments()) { + att.setClassificationSummary(newClassificationSummary); + if (att.getCustomAttributeMap().size() == 3) { + att.setChannel("FAX"); + } + } + // modify existing attachment and task classification + task.setClassificationKey("DOCTYPE_DEFAULT"); // Prio 99, SL P2000D + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getPriority()).isEqualTo(99); + + expDue = workingTimeCalculator.addWorkingTime(task.getPlanned(), Duration.ofDays(16)); + assertThat(task.getDue()).isEqualTo(expDue); + assertThat(task.getAttachments()) + .hasSize(2) + .areExactly( + 1, + new Condition<>( + e -> "FAX".equals(e.getChannel()) && e.getCustomAttributeMap().size() == 3, + "FAX with 3 custom attributes")) + .areExactly( + 1, + new Condition<>( + e -> "ROHRPOST".equals(e.getChannel()) && e.getCustomAttributeMap().size() == 4, + "ROHRPOST with 4 custom attributes")); + } + + @WithAccessId(user = "user-1-1") + @Test + void replaceExistingAttachments() throws Exception { + // setup test + assertThat(task.getAttachments()).isEmpty(); + task.addAttachment(attachment); + Attachment attachment2 = + createExampleAttachment( + "DOCTYPE_DEFAULT", + createObjectReference( + "COMPANY_B", + "SYSTEM_C", + "INSTANCE_C", + "ArchiveId", + "ABC45678901234567890123456789012345678901234567890"), + "E-MAIL", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(4)); + task.addAttachment(attachment2); + task = taskService.updateTask(task); + task = taskService.getTask(task.getId()); + assertThat(task.getAttachments()).hasSize(2); + assertThat(task.getAttachments().get(0).getClassificationSummary().getKey()) + .isEqualTo("DOCTYPE_DEFAULT"); + + Attachment attachment3 = + createExampleAttachment( + "DOCTYPE_DEFAULT", + createObjectReference( + "COMPANY_C", + "SYSTEM_7", + "INSTANCE_7", + "ArchiveId", + "ABC4567890123456789012345678901234567890DEF"), + "DHL", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(4)); + + // replace existing attachments by new via addAttachment call + task.getAttachments().clear(); + task.addAttachment(attachment3); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(1); + assertThat(task.getAttachments().get(0).getChannel()).isEqualTo("DHL"); + task.getAttachments().forEach(at -> assertThat(task.getModified()).isEqualTo(at.getModified())); + // setup environment for 2nd version of replacement (list.add call) + task.getAttachments().add(attachment2); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(2); + assertThat(task.getAttachments().get(1).getChannel()).isEqualTo("E-MAIL"); + // replace attachments + task.getAttachments().clear(); + task.getAttachments().add(attachment3); + task = taskService.updateTask(task); + assertThat(task.getAttachments()).hasSize(1); + assertThat(task.getAttachments().get(0).getChannel()).isEqualTo("DHL"); + } + + @WithAccessId(user = "user-1-1") + @Test + void testPrioDurationOfTaskFromAttachmentsAtUpdate() throws Exception { + + TaskService taskService = taskanaEngine.getTaskService(); + Task newTask = taskService.newTask("USER-1-1", "DOMAIN_A"); + newTask.setClassificationKey("L12010"); // prio 8, SL P7D + newTask.setPrimaryObjRef( + createObjectReference("COMPANY_A", "SYSTEM_A", "INSTANCE_A", "VNR", "1234567")); + + newTask.addAttachment( + createExampleAttachment( + "DOCTYPE_DEFAULT", // prio 99, SL P2000D + createObjectReference( + "COMPANY_A", + "SYSTEM_B", + "INSTANCE_B", + "ArchiveId", + "12345678901234567890123456789012345678901234567890"), + "E-MAIL", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(3))); + newTask.addAttachment( + createExampleAttachment( + "L1060", // prio 1, SL P1D + createObjectReference( + "COMPANY_A", + "SYSTEM_B", + "INSTANCE_B", + "ArchiveId", + "12345678901234567890123456789012345678901234567890"), + "E-MAIL", + Instant.parse("2018-01-15T00:00:00Z"), + createSimpleCustomPropertyMap(3))); + Task createdTask = taskService.createTask(newTask); + + assertThat(createdTask.getId()).isNotNull(); + assertThat(createdTask.getCreator()) + .isEqualTo(taskanaEngine.getCurrentUserContext().getUserid()); + createdTask + .getAttachments() + .forEach(at -> assertThat(createdTask.getModified()).isEqualTo(at.getModified())); + Task readTask = taskService.getTask(createdTask.getId()); + assertThat(readTask).isNotNull(); + assertThat(createdTask.getCreator()) + .isEqualTo(taskanaEngine.getCurrentUserContext().getUserid()); + assertThat(readTask.getAttachments()).isNotNull(); + assertThat(readTask.getAttachments()).hasSize(2); + assertThat(readTask.getAttachments().get(1).getCreated()).isNotNull(); + assertThat(readTask.getAttachments().get(1).getModified()).isNotNull(); + assertThat(readTask.getAttachments().get(0).getCreated()) + .isEqualTo(readTask.getAttachments().get(1).getModified()); + assertThat(readTask.getAttachments().get(0).getObjectReference()).isNotNull(); + + assertThat(readTask.getPriority()).isEqualTo(99); + + Instant expDue = + workingTimeCalculator.addWorkingTime(readTask.getPlanned(), Duration.ofDays(1)); + + assertThat(readTask.getDue()).isEqualTo(expDue); + } + + @WithAccessId(user = "user-1-1") + @Test + void testAddCustomAttributeToAttachment() throws Exception { + + TaskService taskService = taskanaEngine.getTaskService(); + task = + taskService.getTask( + "TKI:000000000000000000000000000000000000"); // class T2000, prio 1, SL P1D + attachment = + createExampleAttachment( + "DOCTYPE_DEFAULT", // prio 99, SL P2000D + createObjectReference( + "COMPANY_A", + "SYSTEM_B", + "INSTANCE_B", + "ArchiveId", + "12345678901234567890123456789012345678901234567890"), + "E-MAIL", + Instant.parse("2018-01-15T00:00:00Z"), + null); + attachment.getCustomAttributeMap().put("TEST_KEY", "TEST_VALUE"); + task.addAttachment(attachment); + taskService.updateTask(task); + Task updatedTask = taskService.getTask("TKI:000000000000000000000000000000000000"); + Attachment updatedAttachment = + updatedTask.getAttachments().stream() + .filter(a -> attachment.getId().equals(a.getId())) + .findFirst() + .orElse(null); + assertThat(updatedAttachment).isNotNull(); + assertThat(updatedTask.getModified()).isEqualTo(updatedAttachment.getModified()); + assertThat(updatedAttachment.getCustomAttributeMap()).containsEntry("TEST_KEY", "TEST_VALUE"); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_UpdatingTaskWithNewAttachmentClassificationNull() { + attachment.setClassificationSummary(null); + task.addAttachment(attachment); + + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessage("Classification of Attachment must not be null."); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_UpdatingTaskWithChangedAttachmentClassificationNull() + throws Exception { + task.addAttachment(attachment); + taskService.updateTask(task); + attachment.setClassificationSummary(null); + + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessage("Classification of Attachment must not be null."); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_UpdatingTaskWithNewAttachmentObjectReferenceNull() + throws Exception { + task.addAttachment(attachment); + taskService.updateTask(task); + + task.removeAttachment(attachment.getId()); + attachment.setObjectReference(null); + task.addAttachment(attachment); + + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessage("ObjectReference of Attachment must not be null."); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_UpdatingTaskWithChangedAttachmentObjectReferenceNull() + throws Exception { + task.addAttachment(attachment); + taskService.updateTask(task); + + task.removeAttachment(attachment.getId()); + attachment.setObjectReference(null); + task.addAttachment(attachment); + + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessage("ObjectReference of Attachment must not be null."); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_UpdatingTaskWithMissingAttachmentClassificationKey() { + ClassificationSummaryImpl classification = new ClassificationSummaryImpl(); + attachment.setClassificationSummary(classification); + task.addAttachment(attachment); + + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(InvalidArgumentException.class) + .hasMessageContaining("ClassificationKey of Attachment must not be empty."); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_ThrowException_When_UpdatingTaskWithNotExistingAttachmentClassification() { + Classification classification = + classificationService.newClassification("NOT_EXISTING", "DOMAIN_A", ""); + attachment.setClassificationSummary(classification); + task.addAttachment(attachment); + + assertThatThrownBy(() -> taskService.updateTask(task)) + .isInstanceOf(ClassificationNotFoundException.class); + } + + @WithAccessId(user = "user-1-1") + @Test + void should_FetchAttachmentClassification_When_UpdatingTaskWithAttachments() throws Exception { + ClassificationSummary classification = + classificationService.newClassification("T2000", "DOMAIN_A", "").asSummary(); + attachment.setClassificationSummary(classification); + task.addAttachment(attachment); + + assertThat(classification.getServiceLevel()).isNull(); + + TaskImpl updatedTask = (TaskImpl) taskService.updateTask(task); + classification = updatedTask.getAttachments().get(0).getClassificationSummary(); + + assertThat(classification.getId()).isNotNull(); + assertThat(classification.getDomain()).isNotNull(); + assertThat(classification.getServiceLevel()).isNotNull(); + } +} diff --git a/lib/taskana-core/src/test/java/acceptance/workbasket/delete/DeleteWorkbasketAccTest.java b/lib/taskana-core/src/test/java/acceptance/workbasket/delete/DeleteWorkbasketAccTest.java index b8e4181ee2..fd3afdf47b 100644 --- a/lib/taskana-core/src/test/java/acceptance/workbasket/delete/DeleteWorkbasketAccTest.java +++ b/lib/taskana-core/src/test/java/acceptance/workbasket/delete/DeleteWorkbasketAccTest.java @@ -185,6 +185,8 @@ void should_MarkWorkbasketForDeletion_When_TryingToDeleteWorkbasketWithTasks() t taskService.forceCompleteTask(task.getId()); task = (TaskImpl) taskService.getTask("TKI:200000000000000000000000000000000066"); taskService.forceCompleteTask(task.getId()); + task = (TaskImpl) taskService.getTask("TKI:000000000000000000000000000000000201"); + taskService.forceCompleteTask(task.getId()); boolean canBeDeletedNow = workbasketService.deleteWorkbasket(wb.getId()); assertThat(canBeDeletedNow).isFalse();