View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.routemanager;
17  
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertFalse;
20  import static org.junit.Assert.assertNotNull;
21  import static org.junit.Assert.assertNull;
22  import static org.junit.Assert.assertTrue;
23  import static org.junit.Assert.fail;
24  
25  import java.util.Collection;
26  import java.util.List;
27  
28  import org.junit.Test;
29  import org.kuali.rice.kew.api.KewApiServiceLocator;
30  import org.kuali.rice.kew.api.WorkflowDocument;
31  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
32  import org.kuali.rice.kew.api.action.ActionRequest;
33  import org.kuali.rice.kew.api.action.ActionRequestType;
34  import org.kuali.rice.kew.api.action.InvalidActionTakenException;
35  import org.kuali.rice.kew.api.action.RecipientType;
36  import org.kuali.rice.kew.api.document.DocumentProcessingQueue;
37  import org.kuali.rice.kew.api.document.DocumentStatus;
38  import org.kuali.rice.kew.api.document.node.RouteNodeInstance;
39  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
40  import org.kuali.rice.kew.service.KEWServiceLocator;
41  import org.kuali.rice.kew.test.KEWTestCase;
42  import org.kuali.rice.kew.test.TestUtilities;
43  import org.kuali.rice.kim.api.group.Group;
44  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
45  import org.kuali.rice.test.BaselineTestCase;
46  
47  @BaselineTestCase.BaselineMode(BaselineTestCase.Mode.CLEAR_DB)
48  public class ExceptionRoutingTest extends KEWTestCase {
49  
50      protected void loadTestData() throws Exception {
51          loadXmlFile("RouteManagerConfig.xml");
52      }
53  
54      protected void setUpAfterDataLoad() throws Exception {
55  		super.setUpAfterDataLoad();
56  		// reset these static constants, otherwise they will cause problems between test runs
57  		ExceptionRoutingTestPostProcessor.THROW_DO_ACTION_TAKEN_EXCEPTION = false;
58  		ExceptionRoutingTestPostProcessor.THROW_ROUTE_DELETE_ROUTE_HEADER_EXCEPTION = false;
59  		ExceptionRoutingTestPostProcessor.THROW_ROUTE_STATUS_CHANGE_EXCEPTION = false;
60  		ExceptionRoutingTestPostProcessor.THROW_ROUTE_STATUS_LEVEL_EXCEPTION = false;
61  		ExceptionRoutingTestPostProcessor.TRANSITIONED_OUT_OF_EXCEPTION_ROUTING = false;
62  		ExceptionRoutingTestPostProcessor.BLOW_UP_ON_TRANSITION_INTO_EXCEPTION = false;
63  	}
64  
65      @Test public void testSequentialExceptionRouting() throws Exception {
66          WorkflowDocument doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("rkirkend"), "ExceptionRoutingSequentialDoc");
67          try {
68              doc.route("");
69              fail("should have thrown routing exception");
70          } catch (Exception e) {
71          }
72  
73          TestUtilities.getExceptionThreader().join();//this is necessary to ensure that the exception request will be generated.
74  
75          doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), doc.getDocumentId());
76          assertEquals("Document status incorrect", DocumentStatus.EXCEPTION, doc.getStatus());
77  
78          List<ActionRequest> actionRequests = KewApiServiceLocator.getWorkflowDocumentService().getRootActionRequests(doc.getDocumentId());
79  
80          assertEquals("Should be a single exception request", 1, actionRequests.size());
81          for (ActionRequest actionRequest : actionRequests) {
82              Group group = KimApiServiceLocator.getGroupService().getGroup(actionRequest.getGroupId());
83              assertTrue("Request should be an exception request." + actionRequest, actionRequest.isExceptionRequest());
84              assertEquals("Complete should be requested", ActionRequestType.COMPLETE, actionRequest.getActionRequested());
85              assertEquals("Request should be a workgroup request", RecipientType.GROUP, actionRequest.getRecipientType());
86              assertEquals("Request should be to 'ExceptionRoutingGroup'", "ExceptionRoutingGroup", group.getName());
87              assertNotNull("annotation cannot be null", actionRequest.getAnnotation());
88              assertFalse("annotation cannot be empty", "".equals(actionRequest.getAnnotation()));
89          }
90  
91      }
92      
93      /**
94       * This tests the solution for KULRICE-4493.  Essentially, the problem was that when the workflow engine
95       * would transition the document to exception status it would invoke the post processor.  If invoking
96       * the post processor raised an exception, that would cause the transaction to get rolled back and the
97       * document would get "stuck" in the ENROUTE state with no pending requests.
98       */
99  //    @Test public void testExceptionRouting_BlowUpOnStatusChangeToException() throws Exception {
100 //    	
101 //    	// first, configure the post processor so that it throws an exception when we call doRouteStatusChange on transition into exception status
102 //    	ExceptionRoutingTestPostProcessor.BLOW_UP_ON_TRANSITION_INTO_EXCEPTION = true;
103 //    	    	
104 //    	WorkflowDocument doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("rkirkend"), "AlwaysExplodeTestDocument");
105 //    	try {
106 //    		doc.route("");
107 //    		fail("We should be in exception routing");
108 //    	} catch (Exception e) {
109 //    	}
110 //
111 //    	TestUtilities.getExceptionThreader().join();//this is necessary to ensure that the exception request will be generated.
112 //    	
113 //    	doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), doc.getDocumentId());
114 //    	assertTrue("document should be in exception routing", doc.isException());
115 //
116 //    }
117 
118 	@Test public void testInvalidActionsInExceptionRouting() throws Exception {
119         WorkflowDocument doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("rkirkend"), "ExceptionRoutingSequentialDoc");
120         try {
121             doc.route("");
122             fail("should have thrown routing exception");
123         } catch (Exception e) {
124             log.info("Expected exception occurred: " + e);
125         }
126 
127         TestUtilities.getExceptionThreader().join();//this is necessary to ensure that the exception request will be generated.
128 
129         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), doc.getDocumentId());
130         assertTrue("Document should be in exception status", doc.isException());
131 
132         try {
133             doc.route("routing a document that is in exception routing");
134             fail("Succeeded in routing document that is in exception routing");
135         } catch (InvalidActionTakenException iate) {
136             log.info("Expected exception occurred: " + iate);
137         }
138     }
139 
140 	@Test public void testParallelExceptionRouting() throws Exception {
141         WorkflowDocument doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("user1"), "ExceptionRoutingParallelDoc");
142         doc.route("");
143         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), doc.getDocumentId());
144         assertTrue("User should have an approve request", doc.isApprovalRequested());
145         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("bmcgough"), doc.getDocumentId());
146         assertTrue("User should have an approve request", doc.isApprovalRequested());
147         List<RouteNodeInstance> nodes = KewApiServiceLocator.getWorkflowDocumentService().getActiveRouteNodeInstances(
148                 doc.getDocumentId());
149 
150         // at this point we should be at RouteNode1 and RouteNode3
151         assertEquals("There should be two active nodes", 2, nodes.size());
152         TestUtilities.assertAtNode(doc, "RouteNode1");
153         TestUtilities.assertAtNode(doc, "RouteNode3");
154 
155         try {
156             doc.approve("");
157             fail("should have generated routing exception");
158         } catch (Exception e) {
159         }
160 
161         TestUtilities.getExceptionThreader().join();//this is necessary to ensure that the exception request will be generated.
162         List<ActionRequest> actionRequests = KewApiServiceLocator.getWorkflowDocumentService().getRootActionRequests(doc.getDocumentId());
163         RouteNodeInstance routeNode1 = null;
164         for (RouteNodeInstance nodeInstanceVO : nodes) {
165         	if (nodeInstanceVO.getName().equals("RouteNode1")) {
166         		routeNode1 = nodeInstanceVO;
167         	}
168         }
169         assertNotNull("Could not locate the routeNode1 node instance.", routeNode1);
170 
171         boolean hasCompleteRequest = false;
172         for (ActionRequest actionRequest : actionRequests) {
173             if (actionRequest.isCompleteRequest()) {
174             	Group group = KimApiServiceLocator.getGroupService().getGroup(actionRequest.getGroupId());
175                 assertTrue("Complete should be requested", actionRequest.isCompleteRequest());
176                 assertTrue("Request should be a workgroup request", actionRequest.isGroupRequest());
177                 assertNull("For exception routing, node instance should have a null id.", actionRequest.getRouteNodeInstanceId());
178                 //assertEquals("Node instance id should be id of routeNode1", routeNode1.getRouteNodeInstanceId(), actionRequest.getNodeInstanceId());
179                 assertEquals("Request should be to 'ExceptionRoutingGroup'", "ExceptionRoutingGroup", group.getName());
180                 hasCompleteRequest = true;
181             }
182         }
183         assertTrue("Document should have had a complete request", hasCompleteRequest);
184         ExplodingRuleAttribute.dontExplode=true;
185 
186         //there should be a single action item to our member of the exception workgroup
187         Collection actionItems = KEWServiceLocator.getActionListService().findByDocumentId(doc.getDocumentId());
188         assertEquals("There should only be action items for the member of our exception workgroup", 1, actionItems.size());
189 
190         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("user3"), doc.getDocumentId());
191         assertTrue("Document should be routing for completion to member of exception workgroup", doc.isCompletionRequested());
192         assertTrue("Document should be in exception status", doc.isException());
193         doc.complete("");
194 
195         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("bmcgough"), doc.getDocumentId());
196         doc.approve("");
197 
198         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), doc.getDocumentId());
199         doc.approve("");
200 
201         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), doc.getDocumentId());
202         doc.approve("");
203 
204         doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jhopf"), doc.getDocumentId());
205         doc.approve("");
206 
207         assertTrue("Document should be final", doc.isFinal());
208     }
209 
210     /**
211      * this tests that the document appropriately gets to exception routing if there is a
212      * problem when transitioning out of first node
213      *
214      * @throws Exception
215      */
216     @Test public void testExceptionInTransitionFromStart() throws Exception {
217 
218     	WorkflowDocument doc = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("rkirkend"), "AdhocTransitionTestDocument");
219     	//blow chunks transitioning out of adhoc to the first route node
220     	ExceptionRoutingTestPostProcessor.THROW_ROUTE_STATUS_LEVEL_EXCEPTION = true;
221 
222     	try {
223     		doc.route("");
224     		fail("We should be in exception routing");
225     	} catch (Exception e) {
226     	}
227 
228     	TestUtilities.getExceptionThreader().join();//this is necessary to ensure that the exception request will be generated.
229     	doc = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), doc.getDocumentId());
230     	assertEquals("document should be in exception routing", DocumentStatus.EXCEPTION, doc.getStatus());
231     }
232 
233     /**
234      * Test to verify the fix for KULWF-669.
235      *
236      * This tests that if we requeue an exception document (through the RouteQueueService) that it doesn't transition
237      * out of exception routing.  Then check that, if we complete it, it properly transitions out of exception routing.
238      */
239     @Test public void testRequeueOfExceptionDocument() throws Exception {
240     	WorkflowDocument document = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("rkirkend"), "AdhocTransitionTestDocument");
241     	document.route("");
242         assertFalse("Document should not be in exception routing.", document.isException());
243 
244         // in fact, at this point it should be routed to jhopf
245         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jhopf"), document.getDocumentId());
246         assertTrue("Jhopf should have an approve.", document.isApprovalRequested());
247 
248         // let's tell it to blow up on level change
249         ExceptionRoutingTestPostProcessor.THROW_ROUTE_STATUS_CHANGE_EXCEPTION = true;
250         try {
251         	document.approve("");
252         	fail("We should be in exception routing");
253     	} catch (Exception e) {
254     	}
255 
256     	TestUtilities.waitForExceptionRouting();
257     	document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), document.getDocumentId());
258         assertEquals("document should be in exception routing", DocumentStatus.EXCEPTION, document.getStatus());
259 
260     	// now requeue the document it should stay at exception routing and the status change callback should not
261     	// indicate a transition out of exception routing (this is to make sure it's not going out of exception
262     	// routing and then right back in)
263     	ExceptionRoutingTestPostProcessor.THROW_ROUTE_STATUS_CHANGE_EXCEPTION = false;
264     	assertFalse("Should not have transitioned out of exception routing yet.", ExceptionRoutingTestPostProcessor.TRANSITIONED_OUT_OF_EXCEPTION_ROUTING);
265     	// the requeue here should happen synchronously because we are using the SynchronousRouteQueue
266     	DocumentRouteHeaderValue routeHeaderValue = KEWServiceLocator.getRouteHeaderService().getRouteHeader(document.getDocumentId());
267         String applicationId = routeHeaderValue.getDocumentType().getApplicationId();
268         DocumentProcessingQueue documentProcessingQueue = KewApiServiceLocator.getDocumentProcessingQueue(document.getDocumentId(), applicationId);
269     	documentProcessingQueue.process(String.valueOf(document.getDocumentId()));
270 
271     	// the document should still be in exception routing
272     	document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), document.getDocumentId());
273         assertEquals("document should be in exception routing", DocumentStatus.EXCEPTION, document.getStatus());
274         assertFalse("document shouldn't have transitioned out of exception routing.", ExceptionRoutingTestPostProcessor.TRANSITIONED_OUT_OF_EXCEPTION_ROUTING);
275 
276         // now turn status change exceptions off and complete the exception request
277         ExceptionRoutingTestPostProcessor.THROW_ROUTE_STATUS_CHANGE_EXCEPTION = false;
278         assertTrue("rkirkend should be in the exception workgroup.", document.isCompletionRequested());
279         document.complete("Completing out of exception routing.");
280 
281         // Note: The behavior here will be a bit different then in a real setting because in these tests the route queue is synchronous so jhopf's original
282         // Approve never actually took place because the transaction was rolled back (because of the exception in the post process).  Therefore, we still
283         // need to take action as him again to push the document to FINAL
284         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jhopf"), document.getDocumentId());
285         assertTrue(document.isApprovalRequested());
286         document.approve("");
287 
288         // document should now be FINAL
289         assertTrue("Document should be FINAL.", document.isFinal());
290 
291         // the status change out of exception routing should have happened
292         assertTrue("Document should have transitioned out of exception routing.", ExceptionRoutingTestPostProcessor.TRANSITIONED_OUT_OF_EXCEPTION_ROUTING);
293     }
294 
295 }