1+ package dev .dsf .process .tutorial .exercise_7 ;
2+
3+ import static dev .dsf .process .tutorial .ConstantsTutorial .CODESYSTEM_VOTING_PROCESS ;
4+ import static dev .dsf .process .tutorial .ConstantsTutorial .CODESYSTEM_VOTING_PROCESS_VALUE_BINARY_QUESTION ;
5+ import static org .junit .Assert .assertEquals ;
6+ import static org .junit .Assert .assertNotNull ;
7+ import static org .junit .Assert .assertTrue ;
8+ import static org .mockito .ArgumentMatchers .any ;
9+
10+ import java .lang .reflect .Constructor ;
11+ import java .lang .reflect .InvocationTargetException ;
12+ import java .lang .reflect .Method ;
13+ import java .lang .reflect .Modifier ;
14+ import java .util .ArrayList ;
15+ import java .util .Arrays ;
16+ import java .util .HashMap ;
17+ import java .util .List ;
18+ import java .util .Map ;
19+ import java .util .Optional ;
20+ import java .util .function .BiConsumer ;
21+ import java .util .function .Predicate ;
22+ import java .util .stream .Collectors ;
23+
24+ import org .camunda .bpm .engine .delegate .DelegateTask ;
25+ import org .camunda .bpm .model .bpmn .Bpmn ;
26+ import org .camunda .bpm .model .bpmn .BpmnModelInstance ;
27+ import org .camunda .bpm .model .bpmn .instance .ExclusiveGateway ;
28+ import org .camunda .bpm .model .bpmn .instance .Process ;
29+ import org .camunda .bpm .model .bpmn .instance .SequenceFlow ;
30+ import org .camunda .bpm .model .bpmn .instance .ServiceTask ;
31+ import org .camunda .bpm .model .bpmn .instance .UserTask ;
32+ import org .camunda .bpm .model .bpmn .instance .camunda .CamundaTaskListener ;
33+ import org .hl7 .fhir .r4 .model .QuestionnaireResponse ;
34+ import org .hl7 .fhir .r4 .model .Task ;
35+ import org .junit .BeforeClass ;
36+ import org .junit .Test ;
37+ import org .junit .runner .RunWith ;
38+ import org .mockito .Mockito ;
39+ import org .mockito .invocation .Invocation ;
40+ import org .mockito .junit .MockitoJUnitRunner ;
41+
42+ import dev .dsf .bpe .v1 .ProcessPluginApi ;
43+ import dev .dsf .bpe .v1 .ProcessPluginDefinition ;
44+ import dev .dsf .bpe .v1 .activity .DefaultUserTaskListener ;
45+ import dev .dsf .bpe .v1 .plugin .ProcessPluginImpl ;
46+ import dev .dsf .bpe .v1 .service .TaskHelper ;
47+ import dev .dsf .bpe .v1 .variables .Variables ;
48+ import dev .dsf .process .tutorial .ConstantsTutorial ;
49+ import dev .dsf .process .tutorial .TestProcessPluginGenerator ;
50+ import dev .dsf .process .tutorial .TutorialProcessPluginDefinition ;
51+
52+ @ RunWith (MockitoJUnitRunner .class )
53+ public class BpmnAndUserTaskListenerTest
54+ {
55+ @ BeforeClass
56+ public static void loadResources ()
57+ {
58+ ProcessPluginDefinition definition = new TutorialProcessPluginDefinition ();
59+ ProcessPluginImpl processPlugin = TestProcessPluginGenerator .generate (definition , false ,
60+ BpmnAndUserTaskListenerTest .class );
61+ boolean initialized = processPlugin .initializeAndValidateResources (ConstantsTutorial .TUTORIAL_DIC_ORGANIZATION_IDENTIFIER );
62+
63+ assertEquals (true , initialized );
64+ }
65+
66+ @ Test
67+ public void testVoteBpmnFile ()
68+ {
69+ String filename = "bpe/vote.bpmn" ;
70+ String processId = "dsfdev_vote" ;
71+ String questionnaireUrl = "http://dsf.dev/fhir/Questionnaire/user-vote|#{version}" ;
72+
73+ BpmnModelInstance model = Bpmn .readModelFromStream (
74+ this .getClass ().getClassLoader ().getResourceAsStream (filename ));
75+ assertNotNull (model );
76+
77+ List <Process > processes = model .getModelElementsByType (Process .class ).stream ()
78+ .filter (p -> processId .equals (p .getId ())).collect (Collectors .toList ());
79+ assertEquals (1 , processes .size ());
80+
81+ Process process = processes .get (0 );
82+
83+ String errorMissingUserTask = "Process '" + processId + "' in file '" + filename + "is missing a User Task" ;
84+ int userTaskCount = process .getChildElementsByType (UserTask .class ).size ();
85+ assertTrue (errorMissingUserTask , userTaskCount > 0 );
86+
87+ String errorMissingCorrectUserTask = "Process '" + processId + "' in file '" + filename + " is missing User Task with incoming flow from exclusive gateway with name 'User Vote?' and outgoing flow to service task with name 'Save User Vote'" ;
88+ Optional <UserTask > optUserTask = process .getChildElementsByType (UserTask .class ).stream ()
89+ .filter (userTask -> userTask .getIncoming ().stream ().anyMatch (isFlowConnectingUserTaskAndExclusiveGateway (userTask )))
90+ .filter (userTask -> userTask .getOutgoing ().stream ().anyMatch (isFlowConnectingUserTaskAndSaveUserVoteServer (userTask ))).findFirst ();
91+ assertTrue (errorMissingCorrectUserTask , optUserTask .isPresent ());
92+ UserTask userTask = optUserTask .get ();
93+
94+ String errorUserTaskIncomingFlowMissingCondition =
95+ "User Task in process '" + processId + "' in file '" + filename + " with name " + userTask .getOutgoing ()
96+ + " is missing condition expression '${userVote}' on incoming flow from exclusive gateway with name 'User Vote?'" ;
97+ assertTrue (errorUserTaskIncomingFlowMissingCondition ,
98+ userTask .getIncoming ().stream ().filter (isFlowConnectingUserTaskAndExclusiveGateway (userTask )).allMatch (hasCorrectConditionExpression ()));
99+
100+ String errorUserTaskIsMissingCorrectFormKey =
101+ "User Task in process '" + processId + "' in file '" + filename + " with name " + userTask .getOutgoing ()
102+ + " is missing Form Key with value " + questionnaireUrl ;
103+ assertEquals (errorUserTaskIsMissingCorrectFormKey , userTask .getCamundaFormKey (), questionnaireUrl );
104+
105+ String packageName = "dev.dsf.process.tutorial.listener" ;
106+ String errorNoUserTaskListenerFound =
107+ "No class extending DefaultUserTaskListener found in package '" + packageName + "'. Unable to verify if User Task has correct Task Listener set." ;
108+ List <Class <? extends DefaultUserTaskListener >> userTaskListeners = Utils .getUserTaskListeners (packageName );
109+ assertTrue (errorNoUserTaskListenerFound , !userTaskListeners .isEmpty ());
110+
111+ String errorUserTaskIsMissingTaskListener =
112+ "User Task in process '" + processId + "' in file '" + filename + " with name " + userTask .getOutgoing ()
113+ + " is missing at least one Task Listener which extends DefaultUserTaskListener. Found classes to add which extend DefaultUserTaskListener: " + userTaskListeners .stream ().map (Class ::getSimpleName ).reduce ("" , (i , next ) -> i + next + " " );
114+ List <CamundaTaskListener > camundaTaskListeners = userTask .getExtensionElements ().getElements ().stream ()
115+ .filter (extensionElement -> extensionElement instanceof CamundaTaskListener )
116+ .map (extensionElement -> (CamundaTaskListener ) extensionElement )
117+ .filter (camundaTaskListener -> userTaskListeners .stream ().anyMatch (userTaskListener -> userTaskListener .getName ().equals (camundaTaskListener .getAttributeValue ("class" ))))
118+ .toList ();
119+ assertTrue (errorUserTaskIsMissingTaskListener , !camundaTaskListeners .isEmpty ());
120+
121+ List <Class <? extends DefaultUserTaskListener >> userTaskListenersInUserTask = userTaskListeners .stream ().filter (userTaskListener -> camundaTaskListeners .stream ().anyMatch (camundaTaskListener -> camundaTaskListener .getAttributeValue ("class" ).equals (userTaskListener .getName ()))).toList ();
122+ userTaskListenersInUserTask .forEach (userTaskListener -> assertEquals (Utils .errorMessageBeanMethod (userTaskListener ), 1 , Utils .countBeanMethods (userTaskListener )));
123+
124+ Map <Class <? extends DefaultUserTaskListener >, List <String >> userTaskListenersWithErrors = userTaskListenersInUserTask .stream ().collect (Collectors .toMap (userTaskListener -> userTaskListener , this ::validateUserTaskListener ));
125+
126+ String errorNoTaskListenerInUserTaskIsValid = "User Task in process '" + processId + "' in file '" + filename + " with name " + userTask .getOutgoing () + " is missing at least one valid UserTaskListener. Errors are: \n " ;
127+ errorNoTaskListenerInUserTaskIsValid += userTaskListenersWithErrors .keySet ().stream ().map (key -> formatErrors (key , userTaskListenersWithErrors .get (key ))).collect (Collectors .joining ());
128+
129+ assertTrue (errorNoTaskListenerInUserTaskIsValid , userTaskListenersWithErrors .keySet ().stream ().anyMatch (userTaskListener -> userTaskListenersWithErrors .get (userTaskListener ).isEmpty ()));
130+ }
131+
132+ private Predicate <SequenceFlow > isFlowConnectingUserTaskAndExclusiveGateway (UserTask userTask )
133+ {
134+ return flow -> flow .getTarget ().equals (userTask ) && flow .getSource () instanceof ExclusiveGateway && flow .getSource ().getName ().equals ("User Vote?" );
135+ }
136+
137+ private Predicate <SequenceFlow > hasCorrectConditionExpression ()
138+ {
139+ return flow -> flow .getConditionExpression ().getTextContent ().equals ("${userVote}" );
140+ }
141+
142+ private Predicate <SequenceFlow > isFlowConnectingUserTaskAndSaveUserVoteServer (UserTask userTask )
143+ {
144+ return flow -> flow .getSource ().equals (userTask ) && flow .getTarget () instanceof ServiceTask && flow .getTarget ().getName ().equals ("Save User Vote" );
145+ }
146+
147+ private String formatErrors (Class <? extends DefaultUserTaskListener > userTaskListener , List <String > errors )
148+ {
149+ String formatted = "" ;
150+
151+ formatted += "Class: " + userTaskListener .getSimpleName () + "\n " ;
152+ formatted += " Errors:\n " + errors .stream ().reduce ("" , (i , next ) -> i + " " + next + "\n " );
153+
154+ return formatted ;
155+ }
156+
157+ private BiConsumer <HashMap <Class <? extends DefaultUserTaskListener >, List <String >>, Class <? extends DefaultUserTaskListener >> putInMap ()
158+ {
159+ return (map , userTaskListener ) -> map .put (userTaskListener , validateUserTaskListener (userTaskListener ));
160+ }
161+
162+ private BiConsumer <HashMap <Class <? extends DefaultUserTaskListener >, List <String >>, HashMap <Class <? extends DefaultUserTaskListener >, List <String >>> combineMaps ()
163+ {
164+ return HashMap ::putAll ;
165+ }
166+
167+ // A UserTaskListener ist considered valid if beforeQuestionnaireResponseCreate() reads the input parameter
168+ // 'binary-question' from the Start Task and set the item.text value of the item with linkId 'binary-question'
169+ // to the value of the input parameter in the QuestionnaireResponse
170+ private List <String > validateUserTaskListener (Class <? extends DefaultUserTaskListener > userTaskListenerClass )
171+ {
172+ List <String > errors = new ArrayList <>();
173+ try
174+ {
175+ Constructor <? extends DefaultUserTaskListener > constructor = userTaskListenerClass .getConstructor (
176+ ProcessPluginApi .class );
177+
178+ ProcessPluginApi apiMock = Mockito .mock (ProcessPluginApi .class );
179+ TaskHelper taskHelperMock = Mockito .mock (TaskHelper .class );
180+ DelegateTask taskMock = Mockito .mock (DelegateTask .class );
181+ QuestionnaireResponse questionnaireResponseMock = Mockito .mock (QuestionnaireResponse .class );
182+ Variables variablesMock = Mockito .mock (Variables .class );
183+ Task startTaskMock = Mockito .mock (Task .class );
184+ QuestionnaireResponse .QuestionnaireResponseItemComponent itemMock = Mockito .mock (QuestionnaireResponse .QuestionnaireResponseItemComponent .class );
185+
186+ String binaryQuestion = "test?" ;
187+
188+ Mockito .lenient ().when (apiMock .getVariables (any ())).thenReturn (variablesMock );
189+ Mockito .lenient ().when (apiMock .getTaskHelper ()).thenReturn (taskHelperMock );
190+ Mockito .lenient ().when (variablesMock .getStartTask ()).thenReturn (startTaskMock );
191+ Mockito .lenient ().when (taskHelperMock .getFirstInputParameterStringValue (startTaskMock , CODESYSTEM_VOTING_PROCESS ,
192+ CODESYSTEM_VOTING_PROCESS_VALUE_BINARY_QUESTION )).thenReturn (Optional .of (binaryQuestion ));
193+ Mockito .lenient ().when (questionnaireResponseMock .getItem ()).thenReturn (List .of (itemMock ));
194+ Mockito .lenient ().when (itemMock .getLinkId ()).thenReturn (CODESYSTEM_VOTING_PROCESS_VALUE_BINARY_QUESTION );
195+ Mockito .lenient ().when (itemMock .getText ()).thenReturn ("foo" );
196+ Mockito .lenient ().when (itemMock .hasText ()).thenReturn (true );
197+
198+ DefaultUserTaskListener listenerSpy = Mockito .spy (constructor .newInstance (apiMock ));
199+ Method method = userTaskListenerClass .getDeclaredMethod ("beforeQuestionnaireResponseCreate" ,
200+ DelegateTask .class , QuestionnaireResponse .class );
201+ method .setAccessible (true );
202+ method .invoke (listenerSpy , taskMock , questionnaireResponseMock );
203+
204+ Optional <Invocation > optionalInvocation = Mockito .mockingDetails (taskHelperMock ).getInvocations ().stream ()
205+ .filter (invocation -> invocation .getMethod ().getName ().equals ("getFirstInputParameterStringValue" ))
206+ .filter (invocation -> invocation .getArguments ()[0 ].equals (startTaskMock ))
207+ .filter (invocation -> invocation .getArguments ()[1 ].equals (CODESYSTEM_VOTING_PROCESS ))
208+ .filter (invocation -> invocation .getArguments ()[2 ].equals (CODESYSTEM_VOTING_PROCESS_VALUE_BINARY_QUESTION ))
209+ .findFirst ();
210+ if (optionalInvocation .isEmpty ()) errors .add ("Expected one call to TaskHelper#getFirstInputParameterStringValue for Start Task and CodeSystem '" + CODESYSTEM_VOTING_PROCESS + "' and Code '" + CODESYSTEM_VOTING_PROCESS_VALUE_BINARY_QUESTION + "'" );
211+
212+ optionalInvocation = Mockito .mockingDetails (itemMock ).getInvocations ().stream ()
213+ .filter (invocation -> invocation .getMethod ().getName ().equals ("setText" ))
214+ .filter (invocation -> invocation .getArguments ()[0 ].equals (binaryQuestion ))
215+ .findFirst ();
216+ if (optionalInvocation .isEmpty ()) errors .add ("Expected one call to QuestionnaireResponseItemComponent#setText for the QuestionnaireResponseItemComponent with linkId 'binary-question'" );
217+ }
218+ catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e )
219+ {
220+ throw new RuntimeException (e );
221+ }
222+
223+ return errors ;
224+ }
225+ }
0 commit comments