From 7f4f4f78f9e2c8fbfd7dd0bb77f19781cafb3b7f Mon Sep 17 00:00:00 2001 From: Enrique Date: Mon, 7 Oct 2024 20:04:20 +0200 Subject: [PATCH] [incubator-kie-issues-1484] Create endpoints for user task (#2018) --- .../quarkus/ServiceDeskProcessTest.java | 171 ++++++++++++-- .../test/java/org/acme/travel/TravelIT.java | 80 ++++--- .../kie/kogito/examples/PersonsRestIT.java | 50 +++- .../config/CustomWorkItemHandlerConfig.java | 41 ---- .../CustomHumanTaskWorkItemHandler.java | 72 ------ .../usertasks/CustomUserTaskLifeCycle.java | 219 ++++++++++++++++++ .../quarkus/ApprovalsProcessTest.java | 69 +++--- .../travels/quarkus/ApprovalsProcessTest.java | 61 +++-- .../org/kie/kogito/hr/HiringProcessIT.java | 36 ++- .../org/acme/travels/ApprovalsProcessIT.java | 41 ++-- .../org/acme/travels/ApprovalsRestIT.java | 35 +-- .../quarkus/ApprovalsProcessTest.java | 51 ++-- .../springboot/ServiceDeskProcessTest.java | 168 ++++++++++++-- .../main/resources/traffic-rules-dmn-wih.bpmn | 4 +- .../org/kie/kogito/examples/persons.bpmn2 | 70 +++--- .../kogito/examples/demo/OrdersProcessIT.java | 21 +- .../kogito/examples/demo/PersonsRestIT.java | 51 +++- .../config/CustomWorkItemHandlerConfig.java | 40 ---- .../CustomHumanTaskWorkItemHandler.java | 73 ------ .../usertasks/CustomUserTaskLifeCycle.java | 218 +++++++++++++++++ .../springboot/ApprovalsProcessTest.java | 86 ++++--- .../springboot/ApprovalsProcessTest.java | 49 ++-- .../oidc/springboot/ApprovalsProcessTest.java | 51 ++-- .../springboot/ApprovalsProcessTest.java | 51 ++-- 24 files changed, 1215 insertions(+), 593 deletions(-) delete mode 100644 kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java delete mode 100644 kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java create mode 100644 kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java delete mode 100644 kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java delete mode 100644 kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java create mode 100644 kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java diff --git a/kogito-quarkus-examples/flexible-process-quarkus/src/test/java/org/kie/kogito/flexible/example/quarkus/ServiceDeskProcessTest.java b/kogito-quarkus-examples/flexible-process-quarkus/src/test/java/org/kie/kogito/flexible/example/quarkus/ServiceDeskProcessTest.java index a5f39fc5cf..3bcad063f1 100644 --- a/kogito-quarkus-examples/flexible-process-quarkus/src/test/java/org/kie/kogito/flexible/example/quarkus/ServiceDeskProcessTest.java +++ b/kogito-quarkus-examples/flexible-process-quarkus/src/test/java/org/kie/kogito/flexible/example/quarkus/ServiceDeskProcessTest.java @@ -18,6 +18,7 @@ */ package org.kie.kogito.flexible.example.quarkus; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -42,6 +43,7 @@ class ServiceDeskProcessTest { private static final String BASE_PATH = "/serviceDesk"; + private static final String USER_TASK_BASE_PATH = "/usertasks/instance"; static { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); @@ -91,31 +93,75 @@ private String createSupportCase() { } private void addSupportComment(String id) { - String location = given() + given() .basePath(BASE_PATH) .contentType(ContentType.JSON) + .queryParam("user", "kelly") .queryParam("group", "support") .when() - .post("/{id}/ReceiveSupportComment", id) + .post("/{id}/ReceiveSupportComment/trigger", id) .then() .statusCode(201) .header("Location", notNullValue()) .extract() .header("Location"); - String taskId = location.substring(location.lastIndexOf("/") + 1); + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "kelly") + .queryParam("group", "support") + .contentType(ContentType.JSON) + .when() + .get() + .then() + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "claim") + .queryParam("user", "kelly") + .queryParam("group", "support") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); Map params = new HashMap<>(); params.put("comment", "Have you tried to turn it off and on again?"); given() - .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) .queryParam("user", "kelly") .queryParam("group", "support") + .body(params) + .when() + .put("/{userTaskId}/outputs", userTaskId) + .then() + .statusCode(200); + + given() .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "kelly") + .queryParam("group", "support") + .body(Collections.emptyMap()) .when() - .body(params) - .post("/{id}/ReceiveSupportComment/{taskId}", id, taskId) + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); + + given() + .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .when() + .get(id) .then() .statusCode(200) .body("supportCase.state", is(State.WAITING_FOR_CUSTOMER.name())) @@ -125,37 +171,82 @@ private void addSupportComment(String id) { } private void addCustomerComment(String id) { - String location = given() + given() .basePath(BASE_PATH) .contentType(ContentType.JSON) + .queryParam("user", "Paco") .queryParam("group", "customer") .when() - .post("/{id}/ReceiveCustomerComment", id) + .post("/{id}/ReceiveCustomerComment/trigger", id) .then() .statusCode(201) .header("Location", notNullValue()) .extract() .header("Location"); - String taskId = location.substring(location.lastIndexOf("/") + 1); + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "Paco") + .queryParam("group", "customer") + .contentType(ContentType.JSON) + .when() + .get() + .then() + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "claim") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); Map params = new HashMap<>(); params.put("comment", "Great idea!"); given() - .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) .queryParam("user", "Paco") .queryParam("group", "customer") + .body(params) + .when() + .put("/{userTaskId}/outputs", userTaskId) + .then() + .statusCode(200); + + given() .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) .when() - .body(params) - .post("/{id}/ReceiveCustomerComment/{taskId}", id, taskId) + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); + + given() + .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .when() + .get(id) .then() .statusCode(200) .body("supportCase.state", is(State.WAITING_FOR_OWNER.name())) .body("supportCase.comments[1].text", is(params.get("comment"))) .body("supportCase.comments[1].author", is("Paco")) .body("supportCase.comments[1].date", notNullValue()); + } private void resolveCase(String id) { @@ -170,9 +261,10 @@ private void resolveCase(String id) { } private void sendQuestionnaire(String id) { - String taskId = given() + given() .basePath(BASE_PATH) .contentType(ContentType.JSON) + .queryParam("user", "Paco") .queryParam("group", "customer") .when() .get("/{id}/tasks", id) @@ -182,24 +274,59 @@ private void sendQuestionnaire(String id) { .body("[0].name", is("Questionnaire")) .extract() .path("[0].id"); + + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "Paco") + .queryParam("group", "customer") + .contentType(ContentType.JSON) + .when() + .get() + .then() + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "claim") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); + Map params = new HashMap<>(); params.put("comment", "Kogito is great!"); params.put("evaluation", 10); given() - .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) .queryParam("user", "Paco") .queryParam("group", "customer") + .body(params) + .when() + .put("/{userTaskId}/outputs", userTaskId) + .then() + .statusCode(200); + + given() .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) .when() - .body(params) - .post("/{id}/Questionnaire/{taskId}/", id, taskId) + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200) - .body("supportCase.state", is(State.CLOSED.name())) - .body("supportCase.questionnaire.comment", is(params.get("comment"))) - .body("supportCase.questionnaire.evaluation", is(params.get("evaluation"))) - .body("supportCase.questionnaire.date", notNullValue()); + .statusCode(200); + } private void checkAllProcessesFinished() { @@ -207,7 +334,7 @@ private void checkAllProcessesFinished() { .basePath(BASE_PATH) .contentType(ContentType.JSON) .when() - .get("/") + .get("") .as(List.class); assertTrue(processes.isEmpty()); diff --git a/kogito-quarkus-examples/kogito-travel-agency/extended/travels/src/test/java/org/acme/travel/TravelIT.java b/kogito-quarkus-examples/kogito-travel-agency/extended/travels/src/test/java/org/acme/travel/TravelIT.java index b04106ebc0..c9aab73b40 100644 --- a/kogito-quarkus-examples/kogito-travel-agency/extended/travels/src/test/java/org/acme/travel/TravelIT.java +++ b/kogito-quarkus-examples/kogito-travel-agency/extended/travels/src/test/java/org/acme/travel/TravelIT.java @@ -41,6 +41,9 @@ import org.kie.kogito.process.WorkItem; import org.kie.kogito.testcontainers.quarkus.InfinispanQuarkusTestResource; import org.kie.kogito.testcontainers.quarkus.KafkaQuarkusTestResource; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @@ -49,6 +52,7 @@ import jakarta.inject.Named; import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.core.StringContains.containsString; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -74,31 +78,31 @@ public class TravelIT { @Named("travels") Process travelsProcess; - private ProcessInstance processInstance; + @Inject + UserTasks userTasks; @BeforeEach public void cleanUp() { abort(travelsProcess.instances()); - processInstance = null; } @Test public void testTravelNoVisaRequired() { - whenNewTravel(TRAVELLER_FROM_POLAND, TRIP_TO_POLAND); - thenProcessIsActive(); - thenHotelAndFlightAreChosen(); + String processInstance = whenNewTravel(TRAVELLER_FROM_POLAND, TRIP_TO_POLAND); + thenProcessIsActive(processInstance); + thenHotelAndFlightAreChosen(processInstance); - whenConfirmTravel(); - thenProcessIsCompleted(); + whenConfirmTravel(processInstance); + thenProcessIsCompleted(processInstance); } @Test public void testTravelVisaRequired() { - whenNewTravel(TRAVELLER_FROM_POLAND, TRIP_TO_US); - thenProcessIsActive(); + String processInstance = whenNewTravel(TRAVELLER_FROM_POLAND, TRIP_TO_US); + thenProcessIsActive(processInstance); - whenAddVisaApplication(); - thenProcessIsActive(); + whenAddVisaApplication(processInstance); + thenProcessIsActive(processInstance); } @Test @@ -114,7 +118,7 @@ public void testProcessMetrics() { PROJECT_ARTIFACT_ID, PROJECT_VERSION))); } - private void whenNewTravel(Traveller traveller, Trip trip) { + private String whenNewTravel(Traveller traveller, Trip trip) { Model m = travelsProcess.createModel(); Map parameters = new HashMap<>(); parameters.put("traveller", traveller); @@ -122,26 +126,38 @@ private void whenNewTravel(Traveller traveller, Trip trip) { m.fromMap(parameters); - this.processInstance = travelsProcess.createInstance(m); - this.processInstance.start(); + ProcessInstance processInstance = travelsProcess.createInstance(m); + processInstance.start(); + return processInstance.id(); } - private void whenConfirmTravel() { - WorkItem workItem = thenNextStepIs(STEP_CONFIRM_TRAVEL, SECURITY_POLICY_JDOE); - Map results = new HashMap<>(); - results.put("approve", true); - processInstance.completeWorkItem(workItem.getId(), results, SECURITY_POLICY_JDOE); + private void whenConfirmTravel(String processInstance) { + WorkItem workItem = thenNextStepIs(processInstance, STEP_CONFIRM_TRAVEL, SECURITY_POLICY_JDOE); + assertThat(workItem).isNotNull(); + + List userTaskInstances = userTasks.instances().findByIdentity(IdentityProviders.of("jdoe")); + userTaskInstances.forEach(ut -> { + ut.setOutput("approve", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), IdentityProviders.of("jdoe")); + }); + } - private void whenAddVisaApplication() { - Map results = new HashMap<>(); - results.put("visaApplication", new VisaApplication("Jan", "Kowalski", "New York", "US", 10, "XXX098765")); - results.put("visaResolution", new VisaResolution(true, "Test reason")); - WorkItem workItem = thenNextStepIs(STEP_VISA_APPLICATION, SECURITY_POLICY_JDOE); - processInstance.completeWorkItem(workItem.getId(), results, SECURITY_POLICY_JDOE); + private void whenAddVisaApplication(String id) { + ProcessInstance processInstance = travelsProcess.instances().findById(id).get(); + WorkItem workItem = thenNextStepIs(processInstance.id(), STEP_VISA_APPLICATION, SECURITY_POLICY_JDOE); + assertThat(workItem).isNotNull(); + List userTaskInstances = userTasks.instances().findByIdentity(IdentityProviders.of("jdoe")); + userTaskInstances.forEach(ut -> { + ut.setOutput("visaApplication", new VisaApplication("Jan", "Kowalski", "New York", "US", 10, "XXX098765")); + ut.setOutput("visaResolution", new VisaResolution(true, "Test reason")); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), IdentityProviders.of("jdoe")); + }); + } - private WorkItem thenNextStepIs(String expected, SecurityPolicy policy) { + private WorkItem thenNextStepIs(String id, String expected, SecurityPolicy policy) { + ProcessInstance processInstance = travelsProcess.instances().findById(id).get(); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); WorkItem next = workItems.get(0); @@ -149,15 +165,17 @@ private WorkItem thenNextStepIs(String expected, SecurityPolicy policy) { return next; } - private void thenProcessIsActive() { - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, this.processInstance.status()); + private void thenProcessIsActive(String id) { + ProcessInstance processInstance = travelsProcess.instances().findById(id).get(); + assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); } - private void thenProcessIsCompleted() { - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, this.processInstance.status()); + private void thenProcessIsCompleted(String id) { + assertThat(travelsProcess.instances().findById(id)).isEmpty(); } - private void thenHotelAndFlightAreChosen() { + private void thenHotelAndFlightAreChosen(String id) { + ProcessInstance processInstance = travelsProcess.instances().findById(id).get(); Model result = (Model) processInstance.variables(); assertEquals(6, result.toMap().size()); Hotel hotel = (Hotel) result.toMap().get("hotel"); diff --git a/kogito-quarkus-examples/process-quarkus-example/src/test/java/org/kie/kogito/examples/PersonsRestIT.java b/kogito-quarkus-examples/process-quarkus-example/src/test/java/org/kie/kogito/examples/PersonsRestIT.java index 011eba9786..3981f47497 100644 --- a/kogito-quarkus-examples/process-quarkus-example/src/test/java/org/kie/kogito/examples/PersonsRestIT.java +++ b/kogito-quarkus-examples/process-quarkus-example/src/test/java/org/kie/kogito/examples/PersonsRestIT.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.examples; +import java.util.Collections; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; @@ -34,18 +36,20 @@ import jakarta.inject.Named; import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.kie.kogito.test.utils.ProcessInstancesTestUtils.abort; -@SuppressWarnings("rawtypes") @QuarkusTest @QuarkusTestResource(value = InfinispanQuarkusTestResource.Conditional.class) @QuarkusTestResource(value = KafkaQuarkusTestResource.Conditional.class) public class PersonsRestIT { + private static String USER_TASK_BASE_PATH = "/usertasks/instance"; + @Inject @Named("persons") Process personProcess; @@ -236,19 +240,45 @@ public void testChildPersonsRestWithSecurityPolicyAndLifeCycles() { .body("[0].name", is("ChildrenHandling")) .extract() .path("[0].id"); + + assertThat(taskId).isNotNull(); // test claim task - String fixedOrderPayload = "{}"; - given().contentType(ContentType.JSON).accept(ContentType.JSON).body(fixedOrderPayload).when().post("/persons/" + firstCreatedId + "/ChildrenHandling/" + taskId + "?phase=claim&user=admin") + + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "admin") + .queryParam("group", "admins") + .contentType(ContentType.JSON) + .when() + .get() .then() - .statusCode(200).body("id", is(firstCreatedId)); - // test release task - given().contentType(ContentType.JSON).accept(ContentType.JSON).body(fixedOrderPayload).when().post("/persons/" + firstCreatedId + "/ChildrenHandling/" + taskId + "?phase=release&user=admin") + .statusCode(200) + .extract() + .body() + .path("[0].id"); + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "release") + .queryParam("user", "admin") + .queryParam("group", "admins") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200).body("id", is(firstCreatedId)); - // test skip - given().contentType(ContentType.JSON).accept(ContentType.JSON).body(fixedOrderPayload).when().post("/persons/" + firstCreatedId + "/ChildrenHandling/" + taskId + "?phase=skip&user=admin") + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "skip") + .queryParam("user", "admin") + .queryParam("group", "admins") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200).body("id", is(firstCreatedId)); + .statusCode(200); // get all persons make sure there is zero given().accept(ContentType.JSON).when().get("/persons").then().statusCode(200) diff --git a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java b/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java deleted file mode 100644 index 745f9f2e06..0000000000 --- a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.acme.travels.config; - -import org.acme.travels.usertasks.CustomHumanTaskWorkItemHandler; -import org.kie.kogito.process.impl.DefaultWorkItemHandlerConfig; - -import jakarta.enterprise.context.ApplicationScoped; - -/** - * Custom work item handler configuration to change default work item handler for user tasks - * to take into account custom phases - * - *
    - *
  • Start
  • - *
  • Complete - an extension to default Complete phase that will allow only completion from started tasks
  • - *
- * - */ -@ApplicationScoped -public class CustomWorkItemHandlerConfig extends DefaultWorkItemHandlerConfig { - { - register("Human Task", new CustomHumanTaskWorkItemHandler()); - } -} diff --git a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java b/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java deleted file mode 100644 index 0a3fa0eabb..0000000000 --- a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.acme.travels.usertasks; - -import java.util.Optional; - -import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItem; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemManager; -import org.kie.kogito.internal.process.workitem.WorkItemLifeCycle; -import org.kie.kogito.internal.process.workitem.WorkItemPhaseState; -import org.kie.kogito.internal.process.workitem.WorkItemTerminationType; -import org.kie.kogito.internal.process.workitem.WorkItemTransition; -import org.kie.kogito.process.workitems.impl.DefaultKogitoWorkItemHandler; -import org.kie.kogito.process.workitems.impl.DefaultWorkItemLifeCycle; -import org.kie.kogito.process.workitems.impl.DefaultWorkItemLifeCyclePhase; - -public class CustomHumanTaskWorkItemHandler extends DefaultKogitoWorkItemHandler { - - public static final String TRANSITION_COMPLETE = "complete"; - public static final String TRANSITION_ABORT = "abort"; - public static final String TRANSITION_ACTIVATE = "activate"; - public static final String TRANSITION_START = "start"; - public static final String TRANSITION_SKIP = "skip"; - - @Override - public WorkItemLifeCycle initialize() { - WorkItemPhaseState initialized = WorkItemPhaseState.initialized(); - WorkItemPhaseState completed = WorkItemPhaseState.of("Completed", WorkItemTerminationType.COMPLETE); - WorkItemPhaseState aborted = WorkItemPhaseState.of("Aborted", WorkItemTerminationType.ABORT); - WorkItemPhaseState activated = WorkItemPhaseState.of("Activated"); - WorkItemPhaseState started = WorkItemPhaseState.of("Started"); - - DefaultWorkItemLifeCyclePhase active = new DefaultWorkItemLifeCyclePhase(TRANSITION_ACTIVATE, initialized, activated, this::activateWorkItemHandler); - DefaultWorkItemLifeCyclePhase start = new DefaultWorkItemLifeCyclePhase(TRANSITION_START, activated, started, this::activateWorkItemHandler); - DefaultWorkItemLifeCyclePhase complete = new DefaultWorkItemLifeCyclePhase(TRANSITION_COMPLETE, started, completed, this::completeWorkItemHandler); - DefaultWorkItemLifeCyclePhase abort = new DefaultWorkItemLifeCyclePhase(TRANSITION_ABORT, started, aborted, this::abortWorkItemHandler); - - return new DefaultWorkItemLifeCycle(active, start, abort, complete); - } - - @Override - public Optional completeWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler handler, KogitoWorkItem workitem, WorkItemTransition transition) { - getUserFromTransition(transition).ifPresent(e -> workitem.setOutput("ActorId", e)); - return Optional.empty(); - } - - private Optional getUserFromTransition(WorkItemTransition transition) { - Optional securityPolicy = transition.policies().stream().filter(SecurityPolicy.class::isInstance).map(SecurityPolicy.class::cast).findAny(); - if (securityPolicy.isPresent()) { - return Optional.ofNullable(securityPolicy.get().getUser()); - } - return Optional.empty(); - } -} diff --git a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java b/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java new file mode 100644 index 0000000000..e372c4ef1b --- /dev/null +++ b/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java @@ -0,0 +1,219 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.acme.travels.usertasks; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.internal.process.workitem.NotAuthorizedException; +import org.kie.kogito.usertask.UserTaskAssignmentStrategy; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskTransitionToken; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTransition; +import org.kie.kogito.usertask.lifecycle.UserTaskLifeCycle; +import org.kie.kogito.usertask.lifecycle.UserTaskState; +import org.kie.kogito.usertask.lifecycle.UserTaskState.TerminationType; +import org.kie.kogito.usertask.lifecycle.UserTaskTransition; +import org.kie.kogito.usertask.lifecycle.UserTaskTransitionException; +import org.kie.kogito.usertask.lifecycle.UserTaskTransitionToken; + +import jakarta.inject.Singleton; + +@Singleton +public class CustomUserTaskLifeCycle implements UserTaskLifeCycle { + public static final String WORKFLOW_ENGINE_USER = "WORKFLOW_ENGINE_USER"; + + public static final String PARAMETER_USER = "USER"; + public static final String PARAMETER_NOTIFY = "NOTIFY"; + + public static final String ACTIVATE = "activate"; + public static final String START = "start"; + public static final String CLAIM = "claim"; + public static final String RELEASE = "release"; + public static final String COMPLETE = "complete"; + public static final String SKIP = "skip"; + public static final String FAIL = "fail"; + + public static final UserTaskState INACTIVE = UserTaskState.initalized(); + public static final UserTaskState ACTIVE = UserTaskState.of("Active"); + public static final UserTaskState STARTED = UserTaskState.of("Started"); + public static final UserTaskState RESERVED = UserTaskState.of("Reserved"); + public static final UserTaskState COMPLETED = UserTaskState.of("Completed", TerminationType.COMPLETED); + public static final UserTaskState OBSOLETE = UserTaskState.of("Obsolete", TerminationType.OBSOLETE); + + private final UserTaskTransition T_NEW_ACTIVE = new DefaultUserTransition(ACTIVATE, INACTIVE, ACTIVE, this::activate); + private final UserTaskTransition T_ACTIVE_START = new DefaultUserTransition(START, ACTIVE, STARTED, this::start); + private final UserTaskTransition T_START_RESERVED = new DefaultUserTransition(CLAIM, STARTED, RESERVED, this::claim); + private final UserTaskTransition T_RESERVED_COMPLETED = new DefaultUserTransition(COMPLETE, RESERVED, COMPLETED, this::complete); + + private final UserTaskTransition T_ACTIVE_SKIPPED = new DefaultUserTransition(SKIP, ACTIVE, OBSOLETE, this::skip); + private final UserTaskTransition T_STARTED_SKIPPED = new DefaultUserTransition(SKIP, STARTED, OBSOLETE, this::skip); + private final UserTaskTransition T_RESERVED_SKIPPED = new DefaultUserTransition(SKIP, RESERVED, OBSOLETE, this::skip); + + private final UserTaskTransition T_RESERVED_ACTIVE = new DefaultUserTransition(RELEASE, RESERVED, ACTIVE, this::release); + + private List transitions; + + public CustomUserTaskLifeCycle() { + transitions = List.of( + T_NEW_ACTIVE, + T_ACTIVE_START, + T_START_RESERVED, + T_ACTIVE_SKIPPED, + T_STARTED_SKIPPED, + T_RESERVED_ACTIVE, + T_RESERVED_COMPLETED, + T_RESERVED_SKIPPED); + } + + @Override + public List allowedTransitions(UserTaskInstance userTaskInstance) { + return transitions.stream().filter(t -> t.source().equals(userTaskInstance.getStatus())).toList(); + } + + @Override + public Optional transition(UserTaskInstance userTaskInstance, UserTaskTransitionToken userTaskTransitionToken, IdentityProvider identityProvider) { + checkPermission(userTaskInstance, identityProvider); + UserTaskTransition transition = transitions.stream() + .filter(t -> t.source().equals(userTaskInstance.getStatus()) && t.id().equals(userTaskTransitionToken.transitionId())) + .findFirst() + .orElseThrow(() -> new UserTaskTransitionException("Invalid transition from " + userTaskInstance.getStatus())); + return transition.executor().execute(userTaskInstance, userTaskTransitionToken, identityProvider); + } + + @Override + public UserTaskTransitionToken newCompleteTransitionToken(UserTaskInstance userTaskInstance, Map data) { + return newTransitionToken(COMPLETE, userTaskInstance.getStatus(), data); + } + + @Override + public UserTaskTransitionToken newAbortTransitionToken(UserTaskInstance userTaskInstance, Map data) { + return newTransitionToken(SKIP, userTaskInstance.getStatus(), data); + } + + @Override + public UserTaskTransitionToken newTransitionToken(String transitionId, UserTaskInstance userTaskInstance, Map data) { + return newTransitionToken(transitionId, userTaskInstance.getStatus(), data); + } + + public UserTaskTransitionToken newTransitionToken(String transitionId, UserTaskState state, Map data) { + UserTaskTransition transition = transitions.stream().filter(e -> e.source().equals(state) && e.id().equals(transitionId)).findAny() + .orElseThrow(() -> new RuntimeException("Invalid transition " + transitionId + " from " + state)); + return new DefaultUserTaskTransitionToken(transition.id(), transition.source(), transition.target(), data); + } + + public Optional activate(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + return Optional.empty(); + } + + public Optional start(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + String user = assignStrategy(userTaskInstance, identityProvider); + if (user != null) { + return Optional.of(newTransitionToken(CLAIM, STARTED, Map.of(PARAMETER_USER, user))); + } + return Optional.empty(); + } + + public Optional claim(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { + if (token.data().containsKey(PARAMETER_USER)) { + defaultUserTaskInstance.setActuaOwner((String) token.data().get(PARAMETER_USER)); + } else { + defaultUserTaskInstance.setActuaOwner(identityProvider.getName()); + } + } + return Optional.empty(); + } + + public Optional release(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { + defaultUserTaskInstance.setActuaOwner(null); + } + return Optional.empty(); + } + + public Optional complete(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + token.data().forEach(userTaskInstance::setOutput); + return Optional.empty(); + } + + public Optional skip(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + if (token.data().containsKey(PARAMETER_NOTIFY)) { + userTaskInstance.getMetadata().put(PARAMETER_NOTIFY, token.data().get(PARAMETER_NOTIFY)); + } + if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { + defaultUserTaskInstance.setActuaOwner(identityProvider.getName()); + } + return Optional.empty(); + } + + private String assignStrategy(UserTaskInstance userTaskInstance, IdentityProvider identityProvider) { + UserTaskAssignmentStrategy assignmentStrategy = userTaskInstance.getUserTask().getAssignmentStrategy(); + return assignmentStrategy.computeAssigment(userTaskInstance, identityProvider).orElse(null); + } + + private void checkPermission(UserTaskInstance userTaskInstance, IdentityProvider identityProvider) { + String user = identityProvider.getName(); + Collection roles = identityProvider.getRoles(); + + if (WORKFLOW_ENGINE_USER.equals(user)) { + return; + } + + // first we check admins + Set adminUsers = userTaskInstance.getAdminUsers(); + if (adminUsers.contains(user)) { + return; + } + + Set userAdminGroups = new HashSet<>(userTaskInstance.getAdminGroups()); + userAdminGroups.retainAll(roles); + if (!userAdminGroups.isEmpty()) { + return; + } + + if (userTaskInstance.getActualOwner() != null && userTaskInstance.getActualOwner().equals(user)) { + return; + } + + if (List.of(INACTIVE, ACTIVE, STARTED).contains(userTaskInstance.getStatus())) { + // there is no user + Set users = new HashSet<>(userTaskInstance.getPotentialUsers()); + users.removeAll(userTaskInstance.getExcludedUsers()); + if (users.contains(identityProvider.getName())) { + return; + } + + Set userPotGroups = new HashSet<>(userTaskInstance.getPotentialGroups()); + userPotGroups.retainAll(roles); + if (!userPotGroups.isEmpty()) { + return; + } + } + + throw new NotAuthorizedException("user " + user + " with roles " + roles + " not autorized to perform an operation on user task " + userTaskInstance.getId()); + } + +} diff --git a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/test/java/org/acme/travels/custom/lifecycle/quarkus/ApprovalsProcessTest.java b/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/test/java/org/acme/travels/custom/lifecycle/quarkus/ApprovalsProcessTest.java index 59af5b65fb..14935d692b 100644 --- a/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/test/java/org/acme/travels/custom/lifecycle/quarkus/ApprovalsProcessTest.java +++ b/kogito-quarkus-examples/process-usertasks-custom-lifecycle-quarkus/src/test/java/org/acme/travels/custom/lifecycle/quarkus/ApprovalsProcessTest.java @@ -25,16 +25,17 @@ import org.acme.travels.Address; import org.acme.travels.Traveller; +import org.acme.travels.usertasks.CustomUserTaskLifeCycle; import org.junit.jupiter.api.Test; import org.kie.kogito.Model; import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.InvalidTransitionException; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; import io.quarkus.test.junit.QuarkusTest; @@ -42,9 +43,9 @@ import jakarta.inject.Named; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; @QuarkusTest public class ApprovalsProcessTest { @@ -53,6 +54,9 @@ public class ApprovalsProcessTest { @Inject Process approvalsProcess; + @Inject + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -76,9 +80,6 @@ public void testApprovalProcess() { assertEquals(1, workItems.size()); Map results = new HashMap<>(); results.put("approved", true); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); processInstance.completeWorkItem(workItems.get(0).getId(), results, policy); policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); @@ -94,16 +95,8 @@ public void testApprovalProcess() { assertEquals(1, workItems.size()); results.put("approved", false); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); processInstance.completeWorkItem(workItems.get(0).getId(), results, policy); - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); - - Model result = (Model) processInstance.variables(); - assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); - assertEquals(result.toMap().get("firstLineApproval"), true); - assertEquals(result.toMap().get("secondLineApproval"), false); + assertThat(approvalsProcess.instances().findById(processInstance.id())).isNotPresent(); } @Test @@ -128,18 +121,15 @@ public void testApprovalProcessViaPhases() { List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - final String wiId = workItems.get(0).getId(); - - // test to make sure you can't complete if the task is not in started state - assertThrows(InvalidTransitionException.class, () -> processInstance.completeWorkItem(wiId, - Collections.singletonMap("approved", true), - SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))))); - - // now test going through phases - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), Collections.singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", Collections.singletonList("managers")); + ut.transition(CustomUserTaskLifeCycle.START, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(CustomUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(CustomUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); @@ -148,21 +138,20 @@ public void testApprovalProcessViaPhases() { identity = IdentityProviders.of("john", Collections.singletonList("managers")); policy = SecurityPolicy.of(identity); - processInstance.workItems(policy); - workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), Collections.singletonMap("approved", false), policy)); - - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); - - Model result = (Model) processInstance.variables(); - assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); - assertEquals(result.toMap().get("firstLineApproval"), true); - assertEquals(result.toMap().get("secondLineApproval"), false); + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", Collections.singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(CustomUserTaskLifeCycle.ACTIVE); + ut.transition(CustomUserTaskLifeCycle.START, Collections.emptyMap(), userIdentity); + ut.transition(CustomUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + ut.setOutput("approved", false); + ut.transition(CustomUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); + + assertThat(approvalsProcess.instances().findById(processInstance.id())).isNotPresent(); } } diff --git a/kogito-quarkus-examples/process-usertasks-quarkus/src/test/java/org/acme/travels/quarkus/ApprovalsProcessTest.java b/kogito-quarkus-examples/process-usertasks-quarkus/src/test/java/org/acme/travels/quarkus/ApprovalsProcessTest.java index b4a1690b2d..a00b83db87 100644 --- a/kogito-quarkus-examples/process-usertasks-quarkus/src/test/java/org/acme/travels/quarkus/ApprovalsProcessTest.java +++ b/kogito-quarkus-examples/process-usertasks-quarkus/src/test/java/org/acme/travels/quarkus/ApprovalsProcessTest.java @@ -27,18 +27,23 @@ import org.acme.travels.Traveller; import org.junit.jupiter.api.Test; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.inject.Named; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -49,6 +54,9 @@ public class ApprovalsProcessTest { @Inject Process approvalsProcess; + @Inject + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -65,8 +73,6 @@ public void testApprovalProcess() { SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))); - processInstance.workItems(policy); - List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); Map results = new HashMap<>(); @@ -77,9 +83,7 @@ public void testApprovalProcess() { workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); - - processInstance.workItems(policy); + policy = SecurityPolicy.of(IdentityProviders.of("manager", Collections.singletonList("managers"))); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); @@ -90,7 +94,7 @@ public void testApprovalProcess() { Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } @@ -109,39 +113,46 @@ public void testApprovalProcessViaPhases() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), Collections.singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("mgmt"))); + policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); - - processInstance.workItems(policy); + identity = IdentityProviders.of("john", singletonList("managers")); + policy = SecurityPolicy.of(identity); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), Collections.singletonMap("approved", false), policy)); + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", singletonList("managers")); + ut.transition(DefaultUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", false); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); + // at this point the process instance does not exits. + + assertThat(approvalsProcess.instances().findById(processInstance.id())).isEmpty(); - Model result = (Model) processInstance.variables(); - assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); - assertEquals(result.toMap().get("firstLineApproval"), true); - assertEquals(result.toMap().get("secondLineApproval"), false); } } diff --git a/kogito-quarkus-examples/process-usertasks-timer-quarkus-with-console/src/test/java/org/kie/kogito/hr/HiringProcessIT.java b/kogito-quarkus-examples/process-usertasks-timer-quarkus-with-console/src/test/java/org/kie/kogito/hr/HiringProcessIT.java index 4de5711322..fda6a39721 100644 --- a/kogito-quarkus-examples/process-usertasks-timer-quarkus-with-console/src/test/java/org/kie/kogito/hr/HiringProcessIT.java +++ b/kogito-quarkus-examples/process-usertasks-timer-quarkus-with-console/src/test/java/org/kie/kogito/hr/HiringProcessIT.java @@ -19,12 +19,14 @@ package org.kie.kogito.hr; import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; import org.kie.kogito.process.Process; @@ -32,6 +34,9 @@ import org.kie.kogito.process.WorkItem; import org.kie.kogito.testcontainers.quarkus.InfinispanQuarkusTestResource; import org.kie.kogito.testcontainers.quarkus.KafkaQuarkusTestResource; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; @@ -39,6 +44,7 @@ import jakarta.inject.Inject; import jakarta.inject.Named; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -51,6 +57,9 @@ public class HiringProcessIT { @Inject Process hiringProcess; + @Inject + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -65,28 +74,33 @@ public void testApprovalProcess() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("jdoe", Arrays.asList("HR", "IT"))); + IdentityProvider jdoeUser = IdentityProviders.of("jdoe", Arrays.asList("HR", "IT")); + SecurityPolicy policy = SecurityPolicy.of(jdoeUser); processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - Map results = new HashMap<>(); - results.put("approve", true); - processInstance.completeWorkItem(workItems.get(0).getId(), results, policy); + + List userTaskInstances = userTasks.instances().findByIdentity(IdentityProviders.of("jdoe")); + userTaskInstances.forEach(ut -> { + ut.setOutput("approve", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), IdentityProviders.of("jdoe")); + }); processInstance.workItems(policy); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - results.put("approve", false); - processInstance.completeWorkItem(workItems.get(0).getId(), results, policy); - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); + userTaskInstances = userTasks.instances().findByIdentity(IdentityProviders.of("jdoe")); + userTaskInstances.forEach(ut -> { + ut.setOutput("approve", false); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), IdentityProviders.of("jdoe")); + }); + + // process does not exist anymore + assertThat(hiringProcess.instances().findById(processInstance.id())).isEmpty(); - Model result = (Model) processInstance.variables(); - assertEquals(3, result.toMap().size()); - assertEquals(true, result.toMap().get("hr_approval")); - assertEquals(false, result.toMap().get("it_approval")); } } diff --git a/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsProcessIT.java b/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsProcessIT.java index 5485991eaf..abcb465b3a 100644 --- a/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsProcessIT.java +++ b/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsProcessIT.java @@ -25,33 +25,35 @@ import org.junit.jupiter.api.Test; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; -import org.kie.kogito.testcontainers.quarkus.KeycloakQuarkusTestResource; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; -import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.inject.Named; import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @QuarkusTest -@QuarkusTestResource(KeycloakQuarkusTestResource.class) public class ApprovalsProcessIT { @Named("approvals") @Inject Process approvalsProcess; + @Inject + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -92,7 +94,7 @@ public void testApprovalProcess() { Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } @@ -118,10 +120,12 @@ public void testApprovalProcessViaPhases() { List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", true), policy)); + IdentityProvider managerUser = IdentityProviders.of("manager"); + List userTasksInstances = userTasks.instances().findByIdentity(managerUser); + userTasksInstances.forEach(ut -> { + ut.setOutput("approved", true); + ut.transition("complete", emptyMap(), managerUser); + }); policy = SecurityPolicy.of(IdentityProviders.of("admin", Arrays.asList("mgmt"))); workItems = processInstance.workItems(policy); @@ -134,16 +138,15 @@ public void testApprovalProcessViaPhases() { workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", false), policy)); + IdentityProvider johnUser = IdentityProviders.of("john", List.of("managers")); + userTasksInstances = userTasks.instances().findByIdentity(johnUser); + userTasksInstances.forEach(ut -> { + ut.transition("claim", emptyMap(), johnUser); + ut.setOutput("approved", false); + ut.transition("complete", emptyMap(), johnUser); + }); - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); + assertThat(approvalsProcess.instances().findById(processInstance.id())).isEmpty(); - Model result = (Model) processInstance.variables(); - assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); - assertEquals(result.toMap().get("firstLineApproval"), true); - assertEquals(result.toMap().get("secondLineApproval"), false); } } diff --git a/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsRestIT.java b/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsRestIT.java index 8ff5644773..edaf5057ff 100644 --- a/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsRestIT.java +++ b/kogito-quarkus-examples/process-usertasks-with-security-oidc-quarkus/src/test/java/org/acme/travels/ApprovalsRestIT.java @@ -28,6 +28,7 @@ import io.restassured.http.ContentType; import static io.restassured.RestAssured.given; +import static java.util.Collections.emptyMap; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; @@ -35,6 +36,8 @@ @QuarkusTestResource(KeycloakQuarkusTestResource.class) public class ApprovalsRestIT { + private static final String USER_TASK_BASE_PATH = "/usertasks/instance"; + @QuarkusTestProperty(name = "quarkus.oidc.auth-server-url") private String keycloakUrl; @@ -50,7 +53,6 @@ public void testStartApprovalUnauthorized() { .statusCode(401); } - @SuppressWarnings("rawtypes") @Test public void testStartApprovalAuthorized() { @@ -85,31 +87,32 @@ public void testStartApprovalAuthorized() { .body("id", is(id)); // tasks assigned in just started approval - - String taskInfo = given() - .auth() - .oauth2(getAccessToken("mary")) - .accept(ContentType.JSON) + String userTaskId = given() + .auth().oauth2(getAccessToken("mary")) + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "mary") + .queryParam("group", "managers") + .contentType(ContentType.JSON) .when() - .get("/approvals/" + id + "/tasks?user=admin&group=managers") + .get() .then() .statusCode(200) - .body("size()", is(1)) - .body("[0].name", is("firstLineApproval")) .extract() + .body() .path("[0].id"); - String payload = "{}"; given() - .auth().oauth2(getAccessToken("mary")) .contentType(ContentType.JSON) - .accept(ContentType.JSON) - .body(payload) + .auth().oauth2(getAccessToken("mary")) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "manager") + .queryParam("group", "managers") + .body(emptyMap()) .when() - .post("/approvals/" + id + "/firstLineApproval/" + taskInfo + "?user=mary&group=managers") + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200) - .body("id", is(id)); + .statusCode(200); // lastly abort the approval given() diff --git a/kogito-quarkus-examples/process-usertasks-with-security-quarkus/src/test/java/org/acme/travels/security/quarkus/ApprovalsProcessTest.java b/kogito-quarkus-examples/process-usertasks-with-security-quarkus/src/test/java/org/acme/travels/security/quarkus/ApprovalsProcessTest.java index 7faa81a290..ca90e9a6e2 100644 --- a/kogito-quarkus-examples/process-usertasks-with-security-quarkus/src/test/java/org/acme/travels/security/quarkus/ApprovalsProcessTest.java +++ b/kogito-quarkus-examples/process-usertasks-with-security-quarkus/src/test/java/org/acme/travels/security/quarkus/ApprovalsProcessTest.java @@ -27,20 +27,23 @@ import org.acme.travels.Traveller; import org.junit.jupiter.api.Test; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; import jakarta.inject.Named; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -51,6 +54,9 @@ public class ApprovalsProcessTest { @Inject Process approvalsProcess; + @Inject + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -92,7 +98,7 @@ public void testApprovalProcess() { Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } @@ -111,38 +117,47 @@ public void testApprovalProcessViaPhases() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("mgmt"))); + policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); - - processInstance.workItems(policy); + identity = IdentityProviders.of("john", singletonList("managers")); + policy = SecurityPolicy.of(identity); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", false), policy)); - + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", singletonList("managers")); + ut.transition(DefaultUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", false); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } diff --git a/kogito-springboot-examples/flexible-process-springboot/src/test/java/org/kie/kogito/flexible/example/springboot/ServiceDeskProcessTest.java b/kogito-springboot-examples/flexible-process-springboot/src/test/java/org/kie/kogito/flexible/example/springboot/ServiceDeskProcessTest.java index d0f5da8495..26b16e4828 100644 --- a/kogito-springboot-examples/flexible-process-springboot/src/test/java/org/kie/kogito/flexible/example/springboot/ServiceDeskProcessTest.java +++ b/kogito-springboot-examples/flexible-process-springboot/src/test/java/org/kie/kogito/flexible/example/springboot/ServiceDeskProcessTest.java @@ -18,6 +18,7 @@ */ package org.kie.kogito.flexible.example.springboot; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -51,6 +52,7 @@ class ServiceDeskProcessTest { int port; private static final String BASE_PATH = "/serviceDesk"; + private static final String USER_TASK_BASE_PATH = "/usertasks/instance"; static { RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(); @@ -68,7 +70,6 @@ void testSupportCaseExample() { addCustomerComment(id); resolveCase(id); sendQuestionnaire(id); - checkAllProcessesFinished(); } @@ -106,32 +107,75 @@ private String createSupportCase() { } private void addSupportComment(String id) { - String location = given() + given() .basePath(BASE_PATH) .contentType(ContentType.JSON) .queryParam("user", "kelly") .queryParam("group", "support") .when() - .post("/{id}/ReceiveSupportComment", id) + .post("/{id}/ReceiveSupportComment/trigger", id) .then() .statusCode(201) .header("Location", notNullValue()) .extract() .header("Location"); - String taskId = location.substring(location.lastIndexOf("/") + 1); + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "kelly") + .queryParam("group", "support") + .contentType(ContentType.JSON) + .when() + .get() + .then() + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "claim") + .queryParam("user", "kelly") + .queryParam("group", "support") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); Map params = new HashMap<>(); params.put("comment", "Have you tried to turn it off and on again?"); given() - .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) .queryParam("user", "kelly") .queryParam("group", "support") + .body(params) + .when() + .put("/{userTaskId}/outputs", userTaskId) + .then() + .statusCode(200); + + given() .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "kelly") + .queryParam("group", "support") + .body(Collections.emptyMap()) .when() - .body(params) - .post("/{id}/ReceiveSupportComment/{taskId}", id, taskId) + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); + + given() + .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .when() + .get(id) .then() .statusCode(200) .body("supportCase.state", is(State.WAITING_FOR_CUSTOMER.name())) @@ -141,38 +185,82 @@ private void addSupportComment(String id) { } private void addCustomerComment(String id) { - String location = given() + given() .basePath(BASE_PATH) .contentType(ContentType.JSON) .queryParam("user", "Paco") .queryParam("group", "customer") .when() - .post("/{id}/ReceiveCustomerComment", id) + .post("/{id}/ReceiveCustomerComment/trigger", id) .then() .statusCode(201) .header("Location", notNullValue()) .extract() .header("Location"); - String taskId = location.substring(location.lastIndexOf("/") + 1); + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "Paco") + .queryParam("group", "customer") + .contentType(ContentType.JSON) + .when() + .get() + .then() + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "claim") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); Map params = new HashMap<>(); params.put("comment", "Great idea!"); given() - .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) .queryParam("user", "Paco") .queryParam("group", "customer") + .body(params) + .when() + .put("/{userTaskId}/outputs", userTaskId) + .then() + .statusCode(200); + + given() .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) .when() - .body(params) - .post("/{id}/ReceiveCustomerComment/{taskId}", id, taskId) + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); + + given() + .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .when() + .get(id) .then() .statusCode(200) .body("supportCase.state", is(State.WAITING_FOR_OWNER.name())) .body("supportCase.comments[1].text", is(params.get("comment"))) .body("supportCase.comments[1].author", is("Paco")) .body("supportCase.comments[1].date", notNullValue()); + } private void resolveCase(String id) { @@ -186,9 +274,8 @@ private void resolveCase(String id) { .body("supportCase.state", is(State.RESOLVED.name())); } - @SuppressWarnings("unchecked") private void sendQuestionnaire(String id) { - String taskId = given() + given() .basePath(BASE_PATH) .contentType(ContentType.JSON) .queryParam("user", "Paco") @@ -201,26 +288,59 @@ private void sendQuestionnaire(String id) { .body("[0].name", is("Questionnaire")) .extract() .path("[0].id"); - ; + + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "Paco") + .queryParam("group", "customer") + .contentType(ContentType.JSON) + .when() + .get() + .then() + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "claim") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) + .then() + .statusCode(200); Map params = new HashMap<>(); params.put("comment", "Kogito is great!"); params.put("evaluation", 10); given() - .basePath(BASE_PATH) + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) .queryParam("user", "Paco") .queryParam("group", "customer") + .body(params) + .when() + .put("/{userTaskId}/outputs", userTaskId) + .then() + .statusCode(200); + + given() .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "complete") + .queryParam("user", "Paco") + .queryParam("group", "customer") + .body(Collections.emptyMap()) .when() - .body(params) - .post("/{id}/Questionnaire/{taskId}", id, taskId) + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200) - .body("supportCase.state", is(State.CLOSED.name())) - .body("supportCase.questionnaire.comment", is(params.get("comment"))) - .body("supportCase.questionnaire.evaluation", is(params.get("evaluation"))) - .body("supportCase.questionnaire.date", notNullValue()); + .statusCode(200); + } private void checkAllProcessesFinished() { diff --git a/kogito-springboot-examples/process-decisions-rest-springboot/src/main/resources/traffic-rules-dmn-wih.bpmn b/kogito-springboot-examples/process-decisions-rest-springboot/src/main/resources/traffic-rules-dmn-wih.bpmn index 6aa79d7bdf..539081fd1d 100644 --- a/kogito-springboot-examples/process-decisions-rest-springboot/src/main/resources/traffic-rules-dmn-wih.bpmn +++ b/kogito-springboot-examples/process-decisions-rest-springboot/src/main/resources/traffic-rules-dmn-wih.bpmn @@ -156,7 +156,7 @@ DMN]]> _38F4877F-50AD-4500-98E1-9B4FD3972291_TaskNameInputX - + @@ -228,7 +228,7 @@ DRL]]> _D8D9E6BA-5940-41F5-BF3A-1A08ADA72A4C_TaskNameInputX - + diff --git a/kogito-springboot-examples/process-springboot-example/src/main/resources/org/kie/kogito/examples/persons.bpmn2 b/kogito-springboot-examples/process-springboot-example/src/main/resources/org/kie/kogito/examples/persons.bpmn2 index ca5242103c..c6a3946d17 100644 --- a/kogito-springboot-examples/process-springboot-example/src/main/resources/org/kie/kogito/examples/persons.bpmn2 +++ b/kogito-springboot-examples/process-springboot-example/src/main/resources/org/kie/kogito/examples/persons.bpmn2 @@ -1,18 +1,18 @@ - - - - - - - - - - - - - + + + + + + + + + + + + + @@ -21,10 +21,10 @@ - + - + @@ -73,7 +73,7 @@ SequenceFlow_5 SequenceFlow_3 - + @@ -82,40 +82,40 @@ SequenceFlow_3 SequenceFlow_4 - - - - + + + + - UserTask_1_TaskNameInputX - UserTask_1_personInputX - UserTask_1_SkippableInputX - UserTask_1_PriorityInputX + UserTask_2_TaskNameInputX + UserTask_2_personInputX + UserTask_2_SkippableInputX + UserTask_2_PriorityInputX - UserTask_1_TaskNameInputX + UserTask_2_TaskNameInputX - + person - UserTask_1_personInputX + UserTask_2_personInputX - UserTask_1_SkippableInputX + UserTask_2_SkippableInputX - + - UserTask_1_PriorityInputX + UserTask_2_PriorityInputX - + @@ -149,7 +149,7 @@ - + @@ -166,11 +166,11 @@ - + - + @@ -189,7 +189,7 @@ - + diff --git a/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/OrdersProcessIT.java b/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/OrdersProcessIT.java index f19c009b4f..06ea627bd1 100644 --- a/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/OrdersProcessIT.java +++ b/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/OrdersProcessIT.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; import org.kie.kogito.examples.DemoApplication; @@ -36,6 +37,9 @@ import org.kie.kogito.process.WorkItem; import org.kie.kogito.testcontainers.springboot.InfinispanSpringBootTestResource; import org.kie.kogito.testcontainers.springboot.KafkaSpringBootTestResource; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @@ -62,6 +66,9 @@ public class OrdersProcessIT { @Qualifier("demo.orderItems") Process orderItemsProcess; + @Autowired + UserTasks userTasks; + private SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); @Test @@ -86,9 +93,11 @@ public void testOrderProcess() { ProcessInstance childProcessInstance = getFirst(orderItemProcesses); - List workItems = childProcessInstance.workItems(policy); - assertEquals(1, workItems.size()); - childProcessInstance.completeWorkItem(workItems.get(0).getId(), null, policy); + IdentityProvider johnUser = IdentityProviders.of("john", Collections.singletonList("managers")); + List userTaskInstances = userTasks.instances().findByIdentity(johnUser); + userTaskInstances.forEach(ut -> { + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), johnUser); + }); assertEquals(ProcessInstance.STATE_COMPLETED, childProcessInstance.status()); Optional pi = orderProcess.instances().findById(processInstance.id()); @@ -133,7 +142,11 @@ public void testOrderProcessWithError() { List workItems = childProcessInstance.workItems(policy); assertEquals(1, workItems.size()); - childProcessInstance.completeWorkItem(workItems.get(0).getId(), null, policy); + IdentityProvider johnUser = IdentityProviders.of("john", Collections.singletonList("managers")); + List userTaskInstances = userTasks.instances().findByIdentity(johnUser); + userTaskInstances.forEach(ut -> { + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), johnUser); + }); assertEquals(ProcessInstance.STATE_COMPLETED, childProcessInstance.status()); assertEquals(ProcessInstance.STATE_COMPLETED, processInstance.status()); diff --git a/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/PersonsRestIT.java b/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/PersonsRestIT.java index 8935e8648f..2b0c5844ce 100644 --- a/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/PersonsRestIT.java +++ b/kogito-springboot-examples/process-springboot-example/src/test/java/org/kie/kogito/examples/demo/PersonsRestIT.java @@ -18,6 +18,8 @@ */ package org.kie.kogito.examples.demo; +import java.util.Collections; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledIfSystemProperty; @@ -38,6 +40,7 @@ import io.restassured.http.ContentType; import static io.restassured.RestAssured.given; +import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.CoreMatchers.notNullValue; import static org.junit.jupiter.api.Assertions.assertNotEquals; @@ -49,6 +52,7 @@ @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = DemoApplication.class) @ContextConfiguration(initializers = { InfinispanSpringBootTestResource.Conditional.class, KafkaSpringBootTestResource.Conditional.class }) public class PersonsRestIT { + private static final String USER_TASK_BASE_PATH = "/usertasks/instance"; @Autowired @Qualifier("persons") @@ -244,19 +248,44 @@ public void testChildPersonsRestWithSecurityPolicyAndLifeCycles() { .extract() .path("[0].id"); + assertThat(taskId).isNotNull(); // test claim task - String fixedOrderPayload = "{}"; - given().contentType(ContentType.JSON).accept(ContentType.JSON).body(fixedOrderPayload).when().post("/persons/" + firstCreatedId + "/ChildrenHandling/" + taskId + "?phase=claim&user=admin") + String userTaskId = given() + .basePath(USER_TASK_BASE_PATH) + .queryParam("user", "admin") + .queryParam("group", "admins") + .contentType(ContentType.JSON) + .when() + .get() .then() - .statusCode(200).body("id", is(firstCreatedId)); - // test release task - given().contentType(ContentType.JSON).accept(ContentType.JSON).body(fixedOrderPayload).when().post("/persons/" + firstCreatedId + "/ChildrenHandling/" + taskId + "?phase=release&user=admin") + .statusCode(200) + .extract() + .body() + .path("[0].id"); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "release") + .queryParam("user", "admin") + .queryParam("group", "admins") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200).body("id", is(firstCreatedId)); - // test skip - given().contentType(ContentType.JSON).accept(ContentType.JSON).body(fixedOrderPayload).when().post("/persons/" + firstCreatedId + "/ChildrenHandling/" + taskId + "?phase=skip&user=admin") + .statusCode(200); + + given() + .contentType(ContentType.JSON) + .basePath(USER_TASK_BASE_PATH) + .queryParam("transitionId", "skip") + .queryParam("user", "admin") + .queryParam("group", "admins") + .body(Collections.emptyMap()) + .when() + .post("/{userTaskId}/transition", userTaskId) .then() - .statusCode(200).body("id", is(firstCreatedId)); + .statusCode(200); // get all persons make sure there is zero given().accept(ContentType.JSON).when().get("/persons").then().statusCode(200) @@ -272,7 +301,7 @@ public void testPersonsRestStartFromUserTask() { // test new person String addPersonPayload = "{\"person\" : {\"name\" : \"Jane Doe\", \"age\" : 30}}"; String firstCreatedId = given().contentType(ContentType.JSON).accept(ContentType.JSON) - .header("X-KOGITO-StartFromNode", "UserTask_1")// this instructs to start from user task and skip any node before it + .header("X-KOGITO-StartFromNode", "UserTask_2")// this instructs to start from user task and skip any node before it .body(addPersonPayload).when() .post("/persons").then().statusCode(201) .body("id", notNullValue(), "person.adult", is(false))// since rule evaluation was skipped adult is still false even though age is about the 18 limit @@ -445,7 +474,7 @@ public void testChildPersonsRestCancelAndTriggerNodeViaMgmtInterface() { .statusCode(200); // then trigger new node instance via management interface - given().contentType(ContentType.JSON).accept(ContentType.JSON).when().post("/management/processes/persons/instances/" + firstCreatedId + "/nodes/UserTask_1").then() + given().contentType(ContentType.JSON).accept(ContentType.JSON).when().post("/management/processes/persons/instances/" + firstCreatedId + "/nodes/UserTask_2").then() .statusCode(200); taskId = given() diff --git a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java b/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java deleted file mode 100644 index 7727ba2c44..0000000000 --- a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/config/CustomWorkItemHandlerConfig.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.acme.travels.config; - -import org.acme.travels.usertasks.CustomHumanTaskWorkItemHandler; -import org.kie.kogito.process.impl.DefaultWorkItemHandlerConfig; -import org.springframework.stereotype.Component; - -/** - * Custom work item handler configuration to change default work item handler for user tasks - * to take into account custom phases - * - *
    - *
  • Start
  • - *
  • Complete - an extension to default Complete phase that will allow only completion from started tasks
  • - *
- * - */ -@Component -public class CustomWorkItemHandlerConfig extends DefaultWorkItemHandlerConfig { - { - register("Human Task", new CustomHumanTaskWorkItemHandler()); - } -} \ No newline at end of file diff --git a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java b/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java deleted file mode 100644 index aa22513441..0000000000 --- a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomHumanTaskWorkItemHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -package org.acme.travels.usertasks; - -import java.util.Optional; - -import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItem; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemManager; -import org.kie.kogito.internal.process.workitem.WorkItemLifeCycle; -import org.kie.kogito.internal.process.workitem.WorkItemPhaseState; -import org.kie.kogito.internal.process.workitem.WorkItemTerminationType; -import org.kie.kogito.internal.process.workitem.WorkItemTransition; -import org.kie.kogito.process.workitems.impl.DefaultKogitoWorkItemHandler; -import org.kie.kogito.process.workitems.impl.DefaultWorkItemLifeCycle; -import org.kie.kogito.process.workitems.impl.DefaultWorkItemLifeCyclePhase; - -public class CustomHumanTaskWorkItemHandler extends DefaultKogitoWorkItemHandler { - - public static final String TRANSITION_COMPLETE = "complete"; - public static final String TRANSITION_ABORT = "abort"; - public static final String TRANSITION_ACTIVATE = "activate"; - public static final String TRANSITION_START = "start"; - public static final String TRANSITION_SKIP = "skip"; - - @Override - public WorkItemLifeCycle initialize() { - WorkItemPhaseState initialized = WorkItemPhaseState.initialized(); - WorkItemPhaseState completed = WorkItemPhaseState.of("Completed", WorkItemTerminationType.COMPLETE); - WorkItemPhaseState aborted = WorkItemPhaseState.of("Aborted", WorkItemTerminationType.ABORT); - WorkItemPhaseState activated = WorkItemPhaseState.of("Activated"); - WorkItemPhaseState started = WorkItemPhaseState.of("Started"); - - DefaultWorkItemLifeCyclePhase active = new DefaultWorkItemLifeCyclePhase(TRANSITION_ACTIVATE, initialized, activated, this::activateWorkItemHandler); - DefaultWorkItemLifeCyclePhase start = new DefaultWorkItemLifeCyclePhase(TRANSITION_START, activated, started, this::activateWorkItemHandler); - DefaultWorkItemLifeCyclePhase complete = new DefaultWorkItemLifeCyclePhase(TRANSITION_COMPLETE, started, completed, this::completeWorkItemHandler); - DefaultWorkItemLifeCyclePhase abort = new DefaultWorkItemLifeCyclePhase(TRANSITION_ABORT, started, aborted, this::abortWorkItemHandler); - - return new DefaultWorkItemLifeCycle(active, start, abort, complete); - } - - @Override - public Optional completeWorkItemHandler(KogitoWorkItemManager manager, KogitoWorkItemHandler handler, KogitoWorkItem workitem, WorkItemTransition transition) { - getUserFromTransition(transition).ifPresent(e -> workitem.setOutput("ActorId", e)); - return Optional.empty(); - } - - private Optional getUserFromTransition(WorkItemTransition transition) { - Optional securityPolicy = transition.policies().stream().filter(SecurityPolicy.class::isInstance).map(SecurityPolicy.class::cast).findAny(); - if (securityPolicy.isPresent()) { - return Optional.ofNullable(securityPolicy.get().getUser()); - } - return Optional.empty(); - } -} diff --git a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java b/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java new file mode 100644 index 0000000000..fe1c4b7365 --- /dev/null +++ b/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/main/java/org/acme/travels/usertasks/CustomUserTaskLifeCycle.java @@ -0,0 +1,218 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.acme.travels.usertasks; + +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import org.kie.kogito.auth.IdentityProvider; +import org.kie.kogito.internal.process.workitem.NotAuthorizedException; +import org.kie.kogito.usertask.UserTaskAssignmentStrategy; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.impl.DefaultUserTaskInstance; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskTransitionToken; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTransition; +import org.kie.kogito.usertask.lifecycle.UserTaskLifeCycle; +import org.kie.kogito.usertask.lifecycle.UserTaskState; +import org.kie.kogito.usertask.lifecycle.UserTaskState.TerminationType; +import org.kie.kogito.usertask.lifecycle.UserTaskTransition; +import org.kie.kogito.usertask.lifecycle.UserTaskTransitionException; +import org.kie.kogito.usertask.lifecycle.UserTaskTransitionToken; +import org.springframework.stereotype.Component; + +@Component +public class CustomUserTaskLifeCycle implements UserTaskLifeCycle { + public static final String WORKFLOW_ENGINE_USER = "WORKFLOW_ENGINE_USER"; + + public static final String PARAMETER_USER = "USER"; + public static final String PARAMETER_NOTIFY = "NOTIFY"; + + public static final String ACTIVATE = "activate"; + public static final String START = "start"; + public static final String CLAIM = "claim"; + public static final String RELEASE = "release"; + public static final String COMPLETE = "complete"; + public static final String SKIP = "skip"; + public static final String FAIL = "fail"; + + public static final UserTaskState INACTIVE = UserTaskState.initalized(); + public static final UserTaskState ACTIVE = UserTaskState.of("Active"); + public static final UserTaskState STARTED = UserTaskState.of("Started"); + public static final UserTaskState RESERVED = UserTaskState.of("Reserved"); + public static final UserTaskState COMPLETED = UserTaskState.of("Completed", TerminationType.COMPLETED); + public static final UserTaskState OBSOLETE = UserTaskState.of("Obsolete", TerminationType.OBSOLETE); + + private final UserTaskTransition T_NEW_ACTIVE = new DefaultUserTransition(ACTIVATE, INACTIVE, ACTIVE, this::activate); + private final UserTaskTransition T_ACTIVE_START = new DefaultUserTransition(START, ACTIVE, STARTED, this::start); + private final UserTaskTransition T_START_RESERVED = new DefaultUserTransition(CLAIM, STARTED, RESERVED, this::claim); + private final UserTaskTransition T_RESERVED_COMPLETED = new DefaultUserTransition(COMPLETE, RESERVED, COMPLETED, this::complete); + + private final UserTaskTransition T_ACTIVE_SKIPPED = new DefaultUserTransition(SKIP, ACTIVE, OBSOLETE, this::skip); + private final UserTaskTransition T_STARTED_SKIPPED = new DefaultUserTransition(SKIP, STARTED, OBSOLETE, this::skip); + private final UserTaskTransition T_RESERVED_SKIPPED = new DefaultUserTransition(SKIP, RESERVED, OBSOLETE, this::skip); + + private final UserTaskTransition T_RESERVED_ACTIVE = new DefaultUserTransition(RELEASE, RESERVED, ACTIVE, this::release); + + private List transitions; + + public CustomUserTaskLifeCycle() { + transitions = List.of( + T_NEW_ACTIVE, + T_ACTIVE_START, + T_START_RESERVED, + T_ACTIVE_SKIPPED, + T_STARTED_SKIPPED, + T_RESERVED_ACTIVE, + T_RESERVED_COMPLETED, + T_RESERVED_SKIPPED); + } + + @Override + public List allowedTransitions(UserTaskInstance userTaskInstance) { + return transitions.stream().filter(t -> t.source().equals(userTaskInstance.getStatus())).toList(); + } + + @Override + public Optional transition(UserTaskInstance userTaskInstance, UserTaskTransitionToken userTaskTransitionToken, IdentityProvider identityProvider) { + checkPermission(userTaskInstance, identityProvider); + UserTaskTransition transition = transitions.stream() + .filter(t -> t.source().equals(userTaskInstance.getStatus()) && t.id().equals(userTaskTransitionToken.transitionId())) + .findFirst() + .orElseThrow(() -> new UserTaskTransitionException("Invalid transition from " + userTaskInstance.getStatus())); + return transition.executor().execute(userTaskInstance, userTaskTransitionToken, identityProvider); + } + + @Override + public UserTaskTransitionToken newCompleteTransitionToken(UserTaskInstance userTaskInstance, Map data) { + return newTransitionToken(COMPLETE, userTaskInstance.getStatus(), data); + } + + @Override + public UserTaskTransitionToken newAbortTransitionToken(UserTaskInstance userTaskInstance, Map data) { + return newTransitionToken(SKIP, userTaskInstance.getStatus(), data); + } + + @Override + public UserTaskTransitionToken newTransitionToken(String transitionId, UserTaskInstance userTaskInstance, Map data) { + return newTransitionToken(transitionId, userTaskInstance.getStatus(), data); + } + + public UserTaskTransitionToken newTransitionToken(String transitionId, UserTaskState state, Map data) { + UserTaskTransition transition = transitions.stream().filter(e -> e.source().equals(state) && e.id().equals(transitionId)).findAny() + .orElseThrow(() -> new RuntimeException("Invalid transition " + transitionId + " from " + state)); + return new DefaultUserTaskTransitionToken(transition.id(), transition.source(), transition.target(), data); + } + + public Optional activate(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + return Optional.empty(); + } + + public Optional start(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + String user = assignStrategy(userTaskInstance, identityProvider); + if (user != null) { + return Optional.of(newTransitionToken(CLAIM, STARTED, Map.of(PARAMETER_USER, user))); + } + return Optional.empty(); + } + + public Optional claim(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { + if (token.data().containsKey(PARAMETER_USER)) { + defaultUserTaskInstance.setActuaOwner((String) token.data().get(PARAMETER_USER)); + } else { + defaultUserTaskInstance.setActuaOwner(identityProvider.getName()); + } + } + return Optional.empty(); + } + + public Optional release(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { + defaultUserTaskInstance.setActuaOwner(null); + } + return Optional.empty(); + } + + public Optional complete(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + token.data().forEach(userTaskInstance::setOutput); + return Optional.empty(); + } + + public Optional skip(UserTaskInstance userTaskInstance, UserTaskTransitionToken token, IdentityProvider identityProvider) { + if (token.data().containsKey(PARAMETER_NOTIFY)) { + userTaskInstance.getMetadata().put(PARAMETER_NOTIFY, token.data().get(PARAMETER_NOTIFY)); + } + if (userTaskInstance instanceof DefaultUserTaskInstance defaultUserTaskInstance) { + defaultUserTaskInstance.setActuaOwner(identityProvider.getName()); + } + return Optional.empty(); + } + + private String assignStrategy(UserTaskInstance userTaskInstance, IdentityProvider identityProvider) { + UserTaskAssignmentStrategy assignmentStrategy = userTaskInstance.getUserTask().getAssignmentStrategy(); + return assignmentStrategy.computeAssigment(userTaskInstance, identityProvider).orElse(null); + } + + private void checkPermission(UserTaskInstance userTaskInstance, IdentityProvider identityProvider) { + String user = identityProvider.getName(); + Collection roles = identityProvider.getRoles(); + + if (WORKFLOW_ENGINE_USER.equals(user)) { + return; + } + + // first we check admins + Set adminUsers = userTaskInstance.getAdminUsers(); + if (adminUsers.contains(user)) { + return; + } + + Set userAdminGroups = new HashSet<>(userTaskInstance.getAdminGroups()); + userAdminGroups.retainAll(roles); + if (!userAdminGroups.isEmpty()) { + return; + } + + if (userTaskInstance.getActualOwner() != null && userTaskInstance.getActualOwner().equals(user)) { + return; + } + + if (List.of(INACTIVE, ACTIVE, STARTED).contains(userTaskInstance.getStatus())) { + // there is no user + Set users = new HashSet<>(userTaskInstance.getPotentialUsers()); + users.removeAll(userTaskInstance.getExcludedUsers()); + if (users.contains(identityProvider.getName())) { + return; + } + + Set userPotGroups = new HashSet<>(userTaskInstance.getPotentialGroups()); + userPotGroups.retainAll(roles); + if (!userPotGroups.isEmpty()) { + return; + } + } + + throw new NotAuthorizedException("user " + user + " with roles " + roles + " not autorized to perform an operation on user task " + userTaskInstance.getId()); + } + +} diff --git a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/test/java/org/acme/travels/custom/lifecycle/springboot/ApprovalsProcessTest.java b/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/test/java/org/acme/travels/custom/lifecycle/springboot/ApprovalsProcessTest.java index e761d10a67..9907beb486 100644 --- a/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/test/java/org/acme/travels/custom/lifecycle/springboot/ApprovalsProcessTest.java +++ b/kogito-springboot-examples/process-usertasks-custom-lifecycle-springboot/src/test/java/org/acme/travels/custom/lifecycle/springboot/ApprovalsProcessTest.java @@ -25,17 +25,19 @@ import org.acme.travels.Address; import org.acme.travels.Traveller; +import org.acme.travels.usertasks.CustomUserTaskLifeCycle; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.InvalidTransitionException; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; import org.kie.kogito.tests.KogitoInfinispanSpringbootApplication; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @@ -44,10 +46,9 @@ import org.springframework.test.context.junit.jupiter.SpringExtension; import static java.util.Collections.singletonList; -import static java.util.Collections.singletonMap; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.fail; @ExtendWith(SpringExtension.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = KogitoInfinispanSpringbootApplication.class) @@ -58,6 +59,9 @@ public class ApprovalsProcessTest { @Qualifier("approvals") Process approvalsProcess; + @Autowired + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -72,7 +76,8 @@ public void testApprovalProcess() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", Collections.singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); processInstance.workItems(policy); @@ -80,15 +85,14 @@ public void testApprovalProcess() { assertEquals(1, workItems.size()); Map results = new HashMap<>(); results.put("approved", true); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); processInstance.completeWorkItem(workItems.get(0).getId(), results, policy); policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", singletonList("managers"))); + identity = IdentityProviders.of("john", Collections.singletonList("managers")); + policy = SecurityPolicy.of(identity); processInstance.workItems(policy); @@ -96,15 +100,8 @@ public void testApprovalProcess() { assertEquals(1, workItems.size()); results.put("approved", false); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); processInstance.completeWorkItem(workItems.get(0).getId(), results, policy); - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); - - Model result = (Model) processInstance.variables(); - assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); - assertEquals(result.toMap().get("firstLineApproval"), true); - assertEquals(result.toMap().get("secondLineApproval"), false); + assertThat(approvalsProcess.instances().findById(processInstance.id())).isNotPresent(); } @Test @@ -121,51 +118,46 @@ public void testApprovalProcessViaPhases() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", Collections.singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); + + processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - final String wiId = workItems.get(0).getId(); - - // test to make sure you can't complete if the task is not in started state - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - - try { - processInstance.transitionWorkItem(wiId, handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", true), - SecurityPolicy.of(IdentityProviders.of("admin", singletonList("managers"))))); - fail("It's not possible to complete non started tasks"); - } catch (InvalidTransitionException e) { - // expected - } - - // now test going through phases - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", Collections.singletonList("managers")); + ut.transition(CustomUserTaskLifeCycle.START, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(CustomUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(CustomUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", singletonList("managers"))); - - processInstance.workItems(policy); + identity = IdentityProviders.of("john", Collections.singletonList("managers")); + policy = SecurityPolicy.of(identity); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - // test that claim can be skipped - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("start", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", false), policy)); + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", Collections.singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(CustomUserTaskLifeCycle.ACTIVE); + ut.transition(CustomUserTaskLifeCycle.START, Collections.emptyMap(), userIdentity); + ut.transition(CustomUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + ut.setOutput("approved", false); + ut.transition(CustomUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); + assertThat(approvalsProcess.instances().findById(processInstance.id())).isNotPresent(); - Model result = (Model) processInstance.variables(); - assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); - assertEquals(result.toMap().get("firstLineApproval"), true); - assertEquals(result.toMap().get("secondLineApproval"), false); } } diff --git a/kogito-springboot-examples/process-usertasks-springboot/src/test/java/org/acme/travels/springboot/ApprovalsProcessTest.java b/kogito-springboot-examples/process-usertasks-springboot/src/test/java/org/acme/travels/springboot/ApprovalsProcessTest.java index 4258f6a1da..da6ba2b9bf 100644 --- a/kogito-springboot-examples/process-usertasks-springboot/src/test/java/org/acme/travels/springboot/ApprovalsProcessTest.java +++ b/kogito-springboot-examples/process-usertasks-springboot/src/test/java/org/acme/travels/springboot/ApprovalsProcessTest.java @@ -28,13 +28,16 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; import org.kie.kogito.tests.KogitoInfinispanSpringbootApplication; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @@ -42,6 +45,8 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit.jupiter.SpringExtension; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -54,6 +59,9 @@ public class ApprovalsProcessTest { @Qualifier("approvals") Process approvalsProcess; + @Autowired + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -91,7 +99,7 @@ public void testApprovalProcess() { Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } @@ -110,38 +118,47 @@ public void testApprovalProcessViaPhases() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), Collections.singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("mgmt"))); + policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); - - processInstance.workItems(policy); + identity = IdentityProviders.of("john", singletonList("managers")); + policy = SecurityPolicy.of(identity); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), Collections.emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), Collections.singletonMap("approved", false), policy)); - + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", singletonList("managers")); + ut.transition(DefaultUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", false); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } diff --git a/kogito-springboot-examples/process-usertasks-with-security-oidc-springboot/src/test/java/org/acme/travels/security/oidc/springboot/ApprovalsProcessTest.java b/kogito-springboot-examples/process-usertasks-with-security-oidc-springboot/src/test/java/org/acme/travels/security/oidc/springboot/ApprovalsProcessTest.java index 8ac6708233..65b3228b9b 100644 --- a/kogito-springboot-examples/process-usertasks-with-security-oidc-springboot/src/test/java/org/acme/travels/security/oidc/springboot/ApprovalsProcessTest.java +++ b/kogito-springboot-examples/process-usertasks-with-security-oidc-springboot/src/test/java/org/acme/travels/security/oidc/springboot/ApprovalsProcessTest.java @@ -28,13 +28,16 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; import org.kie.kogito.springboot.KogitoSpringbootApplication; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @@ -42,8 +45,8 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit.jupiter.SpringExtension; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -56,6 +59,9 @@ public class ApprovalsProcessTest { @Qualifier("approvals") Process approvalsProcess; + @Autowired + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -97,7 +103,7 @@ public void testApprovalProcess() { Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } @@ -116,38 +122,47 @@ public void testApprovalProcessViaPhases() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("mgmt"))); + policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); - - processInstance.workItems(policy); + identity = IdentityProviders.of("john", singletonList("managers")); + policy = SecurityPolicy.of(identity); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", false), policy)); - + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", singletonList("managers")); + ut.transition(DefaultUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", false); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } diff --git a/kogito-springboot-examples/process-usertasks-with-security-springboot/src/test/java/org/acme/travels/security/springboot/ApprovalsProcessTest.java b/kogito-springboot-examples/process-usertasks-with-security-springboot/src/test/java/org/acme/travels/security/springboot/ApprovalsProcessTest.java index 5f56251379..28c3fc24ba 100644 --- a/kogito-springboot-examples/process-usertasks-with-security-springboot/src/test/java/org/acme/travels/security/springboot/ApprovalsProcessTest.java +++ b/kogito-springboot-examples/process-usertasks-with-security-springboot/src/test/java/org/acme/travels/security/springboot/ApprovalsProcessTest.java @@ -28,13 +28,16 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.kie.kogito.Model; +import org.kie.kogito.auth.IdentityProvider; import org.kie.kogito.auth.IdentityProviders; import org.kie.kogito.auth.SecurityPolicy; -import org.kie.kogito.internal.process.workitem.KogitoWorkItemHandler; import org.kie.kogito.process.Process; import org.kie.kogito.process.ProcessInstance; import org.kie.kogito.process.WorkItem; import org.kie.kogito.tests.KogitoInfinispanSpringbootApplication; +import org.kie.kogito.usertask.UserTaskInstance; +import org.kie.kogito.usertask.UserTasks; +import org.kie.kogito.usertask.impl.lifecycle.DefaultUserTaskLifeCycle; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; @@ -42,8 +45,8 @@ import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.context.junit.jupiter.SpringExtension; -import static java.util.Collections.emptyMap; -import static java.util.Collections.singletonMap; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -56,6 +59,9 @@ public class ApprovalsProcessTest { @Qualifier("approvals") Process approvalsProcess; + @Autowired + UserTasks userTasks; + @Test public void testApprovalProcess() { @@ -97,7 +103,7 @@ public void testApprovalProcess() { Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); } @@ -116,38 +122,47 @@ public void testApprovalProcessViaPhases() { processInstance.start(); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_ACTIVE, processInstance.status()); - SecurityPolicy policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("managers"))); + IdentityProvider identity = IdentityProviders.of("admin", singletonList("managers")); + SecurityPolicy policy = SecurityPolicy.of(identity); processInstance.workItems(policy); List workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - KogitoWorkItemHandler handler = approvalsProcess.getKogitoWorkItemHandler(workItems.get(0).getWorkItemHandlerName()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", true), policy)); + List userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("manager", singletonList("managers")); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", true); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); - policy = SecurityPolicy.of(IdentityProviders.of("admin", Collections.singletonList("mgmt"))); + policy = SecurityPolicy.of(IdentityProviders.of("admin", singletonList("mgmt"))); workItems = processInstance.workItems(policy); assertEquals(0, workItems.size()); - policy = SecurityPolicy.of(IdentityProviders.of("john", Collections.singletonList("managers"))); - - processInstance.workItems(policy); + identity = IdentityProviders.of("john", singletonList("managers")); + policy = SecurityPolicy.of(identity); workItems = processInstance.workItems(policy); assertEquals(1, workItems.size()); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("claim", workItems.get(0).getPhaseStatus(), emptyMap(), policy)); - workItems = processInstance.workItems(policy); - processInstance.transitionWorkItem(workItems.get(0).getId(), handler.newTransition("complete", workItems.get(0).getPhaseStatus(), singletonMap("approved", false), policy)); - + userTaskInstances = userTasks.instances().findByIdentity(identity); + assertThat(userTaskInstances).isNotEmpty(); + userTaskInstances.forEach(ut -> { + IdentityProvider userIdentity = IdentityProviders.of("john", singletonList("managers")); + ut.transition(DefaultUserTaskLifeCycle.CLAIM, Collections.emptyMap(), userIdentity); + assertThat(ut.getStatus()).isEqualTo(DefaultUserTaskLifeCycle.RESERVED); + ut.setOutput("approved", false); + ut.transition(DefaultUserTaskLifeCycle.COMPLETE, Collections.emptyMap(), userIdentity); + }); assertEquals(org.kie.api.runtime.process.ProcessInstance.STATE_COMPLETED, processInstance.status()); Model result = (Model) processInstance.variables(); assertEquals(4, result.toMap().size()); - assertEquals(result.toMap().get("approver"), "admin"); + assertEquals(result.toMap().get("approver"), "manager"); assertEquals(result.toMap().get("firstLineApproval"), true); assertEquals(result.toMap().get("secondLineApproval"), false); }