View Javadoc

1   /**
2    * Copyright 2005-2013 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.actions;
17  
18  import junit.framework.Assert;
19  import org.apache.commons.lang.ArrayUtils;
20  import org.junit.Test;
21  import org.kuali.rice.coreservice.api.parameter.Parameter;
22  import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
23  import org.kuali.rice.kew.actionitem.ActionItem;
24  import org.kuali.rice.kew.api.KewApiConstants;
25  import org.kuali.rice.kew.api.KewApiServiceLocator;
26  import org.kuali.rice.kew.api.WorkflowDocument;
27  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
28  import org.kuali.rice.kew.api.action.ActionRequest;
29  import org.kuali.rice.kew.api.action.ActionRequestType;
30  import org.kuali.rice.kew.api.action.ActionType;
31  import org.kuali.rice.kew.api.action.InvalidActionTakenException;
32  import org.kuali.rice.kew.doctype.bo.DocumentType;
33  import org.kuali.rice.kew.doctype.service.impl.KimDocumentTypeAuthorizer;
34  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
35  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
36  import org.kuali.rice.kew.postprocessor.DefaultPostProcessor;
37  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
38  import org.kuali.rice.kew.service.KEWServiceLocator;
39  import org.kuali.rice.kew.test.KEWTestCase;
40  import org.kuali.rice.kim.api.KimConstants;
41  import org.kuali.rice.kim.api.common.template.Template;
42  import org.kuali.rice.kim.api.permission.Permission;
43  import org.kuali.rice.kim.api.role.Role;
44  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
45  import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo;
46  import org.kuali.rice.krad.service.KRADServiceLocator;
47  import org.kuali.rice.krad.util.KRADConstants;
48  
49  import java.util.Collection;
50  import java.util.HashMap;
51  import java.util.List;
52  import java.util.Map;
53  
54  import static org.junit.Assert.*;
55  
56  public class RecallActionTest extends KEWTestCase {
57      /**
58       * test postprocessor for testing afterActionTaken hook
59       */
60      public static class RecallTestPostProcessor extends DefaultPostProcessor {
61          public static ActionType afterActionTakenType;
62          public static ActionTakenEvent afterActionTakenEvent;
63          @Override
64          public ProcessDocReport afterActionTaken(ActionType performed, ActionTakenEvent event) throws Exception {
65              afterActionTakenType = performed;
66              afterActionTakenEvent = event;
67              return super.afterActionTaken(performed, event);
68          }
69      }
70  
71      public static class RecallTestDocumentTypeAuthorizer extends KimDocumentTypeAuthorizer {
72          public static String CUSTOM_RECALL_QUALIFIER_NAME = "Dynamic Qualifier";
73          public static String CUSTOM_RECALL_QUALIFIER_VALUE = "Dynamic Qualifier Value";
74          // we have to use a detail already defined for the recall permission - app doc status seems the most application-controlled
75          public static String CUSTOM_RECALL_DETAIL_NAME = KimConstants.AttributeConstants.APP_DOC_STATUS;
76          public static String CUSTOM_RECALL_DETAIL_VALUE = "Dynamic Recall Permission Detail Value";
77  
78          public static boolean buildPermissionDetailsInvoked = false;
79          public static boolean buildRoleQualifiersInvoked= false;
80  
81          @Override
82          protected Map<String, String> buildDocumentTypePermissionDetails(DocumentType documentType, String documentStatus, String actionRequestedCode, String routeNodeName) {
83              buildPermissionDetailsInvoked = true;
84              Map<String, String> details = super.buildDocumentTypePermissionDetails(documentType, documentStatus, actionRequestedCode, routeNodeName);
85              details.put(CUSTOM_RECALL_DETAIL_NAME, CUSTOM_RECALL_DETAIL_VALUE);
86              return details;
87          }
88  
89          @Override
90          protected Map<String, String> buildDocumentRoleQualifiers(DocumentRouteHeaderValue document, String routeNodeName) {
91              buildRoleQualifiersInvoked = true;
92              Map<String, String> qualifiers = super.buildDocumentRoleQualifiers(document, routeNodeName);
93              qualifiers.put(CUSTOM_RECALL_QUALIFIER_NAME, CUSTOM_RECALL_QUALIFIER_VALUE);
94              return qualifiers;
95          }
96      }
97  
98      private static final String RECALL_TEST_DOC = "RecallTest";
99      private static final String RECALL_TEST_RESTRICTED_DOC = "RecallTestRestricted";
100     private static final String RECALL_TEST_NOROUTING_DOC = "RecallTestNoRouting";
101     private static final String RECALL_TEST_ONLYADHOC_DOC = "RecallTestOnlyAdhoc";
102     private static final String RECALL_NOTIFY_TEST_DOC = "RecallWithPrevNotifyTest";
103     private static final String RECALL_NO_PENDING_NOTIFY_TEST_DOC = "RecallWithoutPendingNotifyTest";
104     private static final String RECALL_NOTIFY_THIRDPARTY_TEST_DOC = "RecallWithThirdPartyNotifyTest";
105 
106     private String EWESTFAL = null;
107     private String JHOPF = null;
108     private String RKIRKEND = null;
109     private String NATJOHNS = null;
110     private String BMCGOUGH = null;
111 
112     protected void loadTestData() throws Exception {
113         loadXmlFile("ActionsConfig.xml");
114     }
115 
116     @Override
117     protected void setUpAfterDataLoad() throws Exception {
118         super.setUpAfterDataLoad();
119         EWESTFAL = getPrincipalIdForName("ewestfal");
120         JHOPF = getPrincipalIdForName("jhopf");
121         RKIRKEND = getPrincipalIdForName("rkirkend");
122         NATJOHNS = getPrincipalIdForName("natjohns");
123         BMCGOUGH = getPrincipalIdForName("bmcgough");
124 
125         RecallTestPostProcessor.afterActionTakenType = null;
126         RecallTestPostProcessor.afterActionTakenEvent = null;
127     }
128 
129     protected void assertAfterActionTakenCalled(ActionType performed, ActionType taken) {
130         assertEquals(performed, RecallTestPostProcessor.afterActionTakenType);
131         assertNotNull(RecallTestPostProcessor.afterActionTakenEvent);
132         assertEquals(taken, RecallTestPostProcessor.afterActionTakenEvent.getActionTaken().getActionTaken());
133     }
134 
135     @Test(expected=InvalidActionTakenException.class) public void testCantRecallUnroutedDoc() {
136         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
137         document.recall("recalling", true);
138     }
139 
140     @Test public void testRecallAsInitiatorBeforeAnyApprovals() throws Exception {
141         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
142         document.route("");
143 
144         document.recall("recalling", true);
145 
146         assertTrue("Document should be recalled", document.isRecalled());
147         assertAfterActionTakenCalled(ActionType.RECALL, ActionType.RECALL);
148 
149         //verify that the document is truly dead - no more action requests or action items.
150 
151         List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId());
152         assertEquals("Should not have any active requests", 0, requests.size());
153 
154         Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId());
155         assertEquals("Should not have any action items", 0, actionItems.size());
156     }
157 
158     @Test public void testRecallValidActionsTaken() throws Exception {
159         // just complete
160         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC);
161         document.route("routing");
162         document.recall("recalling", true);
163 
164         // save and complete
165         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC);
166         document.saveDocument("saving");
167         document.route("routing");
168         document.recall("recalling", true);
169     }
170 
171     @Test
172     public void testRecallInvalidActionsTaken() throws Exception {
173         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_RESTRICTED_DOC);
174         document.route("");
175 
176         document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId());
177         document.approve("");
178 
179         try {
180             document.recall("recalling", true);
181             fail("Recall should NOT have succeeded.  Expected InvalidActionTakenException due to invalid 'APROVE' prior action taken.");
182         } catch (InvalidActionTakenException iate) {
183             assertTrue(iate.getMessage().contains("Invalid prior action taken: 'APPROVE'"));
184         }
185     }
186 
187     @Test public void testRecallOnlyAdhocRouting() throws Exception {
188         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_ONLYADHOC_DOC);
189         // adhoc it to someone to prevent doc from going final - final is itself an invalid state for recall
190         document.adHocToPrincipal(ActionRequestType.APPROVE, "adhoc approve to JHOPF", JHOPF, "adhocing to prevent finalization", true);
191         document.route("routing");
192         try {
193             document.recall("recalling", true);
194             fail("Recall should NOT have succeeded.  Expected InvalidActionTakenException due to absence of non-adhoc route nodes.");
195         } catch (InvalidActionTakenException iate) {
196             assertTrue(iate.getMessage().contains("No non-adhoc route nodes defined"));
197         }
198     }
199 
200     @Test public void testRecallAsInitiatorAfterSingleApproval() throws Exception {
201         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
202         document.route("");
203 
204         document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId());
205         document.approve("");
206 
207         document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId());
208         document.recall("recalling", true);
209 
210         assertTrue("Document should be recalled", document.isRecalled());
211         assertAfterActionTakenCalled(ActionType.RECALL, ActionType.RECALL);
212 
213         //verify that the document is truly dead - no more action requests or action items.
214 
215         List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId());
216         assertEquals("Should not have any active requests", 0, requests.size());
217 
218         Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId());
219         assertEquals("Should not have any action items", 0, actionItems.size());
220 
221         // can't recall recalled doc
222         assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL));
223     }
224 
225     @Test(expected=InvalidActionTakenException.class)
226     public void testRecallInvalidWhenProcessed() throws Exception {
227         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
228         document.route("");
229 
230         for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) {
231             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
232             document.approve("");
233         }
234 
235         document.refresh();
236         assertTrue("Document should be processed", document.isProcessed());
237         assertTrue("Document should be approved", document.isApproved());
238         assertFalse("Document should not be final", document.isFinal());
239 
240         document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId());
241         document.recall("recalling when processed should fail", true);
242     }
243 
244     @Test(expected=InvalidActionTakenException.class)
245     public void testRecallInvalidWhenFinal() throws Exception {
246         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
247         document.route("");
248 
249         for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) {
250             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
251             document.approve("");
252         }
253         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId());
254         document.acknowledge("");
255 
256         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId());
257         document.fyi();
258 
259         for (ActionRequest a: document.getRootActionRequests()) {
260             System.err.println(a);
261             if (a.isAcknowledgeRequest() || a.isFyiRequest()) {
262                 System.err.println(a.getPrincipalId());
263                 System.err.println(KimApiServiceLocator.getIdentityService().getPrincipal(a.getPrincipalId()).getPrincipalName());
264             }
265         }
266 
267         assertFalse("Document should not be processed", document.isProcessed());
268         assertTrue("Document should be approved", document.isApproved());
269         assertTrue("Document should be final", document.isFinal());
270 
271         document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId());
272         document.recall("recalling when processed should fail", true);
273     }
274 
275     @Test public void testRecallToActionListAsInitiatorBeforeAnyApprovals() throws Exception {
276         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
277         document.route("");
278 
279         document.recall("recalling", false);
280 
281         assertTrue("Document should be saved", document.isSaved());
282         assertEquals(1, document.getCurrentNodeNames().size());
283         assertTrue(document.getCurrentNodeNames().contains("AdHoc"));
284         assertAfterActionTakenCalled(ActionType.RECALL, ActionType.COMPLETE);
285 
286         // initiator has completion request
287         assertTrue(document.isCompletionRequested());
288         // can't recall saved doc
289         assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL));
290 
291         // first approver has FYI
292         assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested());
293 
294         document.complete("completing");
295 
296         assertTrue("Document should be enroute", document.isEnroute());
297 
298         assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isApprovalRequested());
299     }
300 
301     private static final String PERM_APP_DOC_STATUS = "recallable by admins";
302     private static final String ROUTE_NODE = "NotifyFirst";
303     private static final String ROUTE_STATUS = "R";
304 
305     protected Permission createRecallPermission(String docType, String appDocStatus, String routeNode, String routeStatus) {
306         return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus);
307     }
308 
309     protected Permission createRouteDocumentPermission(String docType, String appDocStatus, String routeNode, String routeStatus) {
310         return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus);
311     }
312 
313     protected Permission createPermissionForTemplate(String template_ns, String template_name, String permission_ns, String permission_name, String docType, String appDocStatus, String routeNode, String routeStatus) {
314         Template permTmpl = KimApiServiceLocator.getPermissionService().findPermTemplateByNamespaceCodeAndName(template_ns, template_name);
315         assertNotNull(permTmpl);
316         Permission.Builder permission = Permission.Builder.create(permission_ns, permission_name);
317         permission.setDescription(permission_name);
318         permission.setTemplate(Template.Builder.create(permTmpl));
319         Map<String, String> attrs = new HashMap<String, String>();
320         attrs.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, docType);
321         attrs.put(KimConstants.AttributeConstants.APP_DOC_STATUS, appDocStatus);
322         attrs.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, routeNode);
323         attrs.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, routeStatus);
324         permission.setActive(true);
325         permission.setAttributes(attrs);
326 
327         // save the permission and check that's it's wired up correctly
328         Permission perm = KimApiServiceLocator.getPermissionService().createPermission(permission.build());
329         assertEquals(perm.getTemplate().getId(), permTmpl.getId());
330         int num = 1;
331         if (appDocStatus != null) num++;
332         if (routeNode != null) num++;
333         if (routeStatus != null) num++;
334         assertEquals(num, perm.getAttributes().size());
335         assertEquals(docType, perm.getAttributes().get(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME));
336         assertEquals(appDocStatus, perm.getAttributes().get(KimConstants.AttributeConstants.APP_DOC_STATUS));
337         assertEquals(routeNode, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_NODE_NAME));
338         assertEquals(routeStatus, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_STATUS_CODE));
339 
340         return perm;
341     }
342 
343     // disable the existing Recall Permission assigned to Initiator Role for test purposes
344     protected void disableInitiatorRecallPermission() {
345         Permission p = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName("KR-WKFLW", "Recall Document");
346         Permission.Builder pb = Permission.Builder.create(p);
347         pb.setActive(false);
348         KimApiServiceLocator.getPermissionService().updatePermission(pb.build());
349     }
350 
351     // setter for Kim Priority Parameter (used for useKimPermission method call)
352     protected void setKimPriorityOnDocumentTypeParameterValue(String parameterValue) {
353         if(CoreFrameworkServiceLocator.getParameterService().parameterExists(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND)) {
354             Parameter kimPriorityOverDocTypePolicyParameter = CoreFrameworkServiceLocator.getParameterService().getParameter(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND);
355             Parameter.Builder b = Parameter.Builder.create(kimPriorityOverDocTypePolicyParameter);
356             b.setValue(parameterValue);
357             CoreFrameworkServiceLocator.getParameterService().updateParameter(b.build());
358         }
359     }
360 
361     protected String getKimPriorityOnDocumentTypeParameterValue() {
362         if(CoreFrameworkServiceLocator.getParameterService().parameterExists(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND)) {
363             return CoreFrameworkServiceLocator.getParameterService().getParameter(KewApiConstants.KEW_NAMESPACE, KRADConstants.DetailTypes.ALL_DETAIL_TYPE, KewApiConstants.KIM_PRIORITY_ON_DOC_TYP_PERMS_IND).getValue();
364         }
365         return null;
366     }
367 
368 
369     /**
370      * Tests that a new permission can be configured with the Recall Permission template and that matching works correctly
371      * against the new permission
372      */
373     @Test public void testRecallPermissionMatching() {
374         disableInitiatorRecallPermission();
375         createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS);
376 
377         Map<String, String> details = new HashMap<String, String>();
378         details.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, RECALL_TEST_DOC);
379         details.put(KimConstants.AttributeConstants.APP_DOC_STATUS, PERM_APP_DOC_STATUS);
380         details.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, ROUTE_NODE);
381         details.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, ROUTE_STATUS);
382 
383         // test all single field mismatches
384         for (Map.Entry<String, String> entry: details.entrySet()) {
385             Map<String, String> testDetails = new HashMap<String, String>(details);
386             // change a single detail to a non-matching value
387             testDetails.put(entry.getKey(), entry.getValue() + " BOGUS ");
388             assertFalse("non-matching " + entry.getKey() + " detail should cause template to not match", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, testDetails));
389         }
390 
391         assertTrue("template should match details", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, details));
392     }
393 
394     @Test public void testRecallPermissionTemplate() throws Exception {
395         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
396         document.route("");
397 
398         // nope, technical admins can't recall
399         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
400         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
401 
402         // create a recall permission for the RECALL_TEST_DOC doctype
403         Permission perm = createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS);
404 
405         // assign the permission to Technical Administrator role
406         Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator");
407         KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId());
408 
409         // our recall permission is assigned to the technical admin role
410 
411         // but the doc will not match...
412         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_NOTIFY_TEST_DOC);
413         document.route(PERM_APP_DOC_STATUS);
414         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
415         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
416 
417         // .. the app doc status will not match...
418         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
419         document.route("");
420         // technical admins can't recall since the app doc status is not correct
421         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
422         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
423 
424         // ... the node will not match ...
425         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
426         document.route("");
427         WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve(""); // approve past notifyfirstnode
428         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
429         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
430 
431         // ... the doc status will not match (not recallable anyway) ...
432         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
433         document.route("");
434         document.cancel("cancelled");
435         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
436         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
437 
438         // everything should match
439         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
440         document.setApplicationDocumentStatus(PERM_APP_DOC_STATUS);
441         document.route("");
442         // now technical admins can recall by virtue of having the recall permission on this doc
443         assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
444         assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
445     }
446 
447     @Test public void testRecallToActionListAsInitiatorAfterApprovals() throws Exception {
448         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_TEST_DOC);
449     }
450 
451     @Test public void testRecallToActionListAsInitiatorWithNotificationAfterApprovals() throws Exception {
452         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_TEST_DOC);
453     }
454 
455     @Test public void testRecallToActionListAsInitiatorWithoutPendingNotificationAfterApprovals() throws Exception {
456         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NO_PENDING_NOTIFY_TEST_DOC);
457     }
458 
459     @Test public void testRecallToActionListAsInitiatorWithThirdPartyNotificationAfterApprovals() throws Exception {
460         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_THIRDPARTY_TEST_DOC);
461     }
462 
463     /**
464      * Tests that the document is returned to the *recaller*'s action list, not the original initiator
465      * @throws Exception
466      */
467     @Test public void testRecallToActionListAsThirdParty() throws Exception {
468         Permission perm = createRecallPermission(RECALL_TEST_DOC, null, null, null);
469         // assign the permission to Technical Administrator role
470         Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator");
471         KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId());
472         // recall as 'admin' user
473         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC);
474     }
475 
476     // the three tests below test permutations of recall permission and derived role assignment
477     protected void assignRoutePermissionToTechAdmin() {
478         // assign Route Document permission to the Technical Administrator role
479         Permission routePerm = createRouteDocumentPermission(RECALL_TEST_DOC, null, null, null);
480         Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator");
481         KimApiServiceLocator.getRoleService().assignPermissionToRole(routePerm.getId(), techadmin.getId());
482     }
483     protected void assignRecallPermissionToDocumentRouters() {
484         // assign Recall permission to the Document Router derived role
485         Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, null, null, null);
486         Role documentRouterDerivedRole = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-WKFLW", "Document Router");
487         KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), documentRouterDerivedRole.getId());
488     }
489     /**
490      * Tests that simply assigning the Route Document permission to the Technical Admin role *without* assigning the
491      * Recall permission to the Document Router derived role, is NOT sufficient to enable recall.
492      */
493     @Test public void testRoutePermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception {
494         assignRoutePermissionToTechAdmin();
495         // recall as 'admin' (Tech Admin) user
496         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false);
497     }
498     /**
499      * Tests that simply assigning the recall permission to the Document Router derived role *without* assigning the
500      * Route Document permission to the Technical Admin role, is NOT sufficient to enable recall.
501      */
502     @Test public void testRecallPermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception {
503         assignRecallPermissionToDocumentRouters();
504         // recall as 'admin' (Tech Admin) user
505         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false);
506     }
507     /**
508      * Tests that we can use the Route Document derived role to assign Recall permission to document routers.
509      */
510     @Test public void testRecallToActionListAsRouterDerivedRole() throws Exception {
511         // assign both! derived role works its magic
512         assignRoutePermissionToTechAdmin();
513         assignRecallPermissionToDocumentRouters();
514         // recall as 'admin' user (Tech Admin) user
515         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC);
516     }
517 
518     /**
519      * Creates a new role with recall permission qualified with doc type and custom app doc status
520      * @param ns role namespace
521      * @param name role name
522      * @param recallPerm the pre-created Recall permission
523      * @return the new recall-capable Role
524      */
525     protected Role createRoleWithRecallPermission(String ns, String name, Permission recallPerm, String roleQualifierName) {
526         // create a new role
527         Role.Builder role = Role.Builder.create();
528         role.setActive(true);
529         role.setDescription("RecallTest custom recall role");
530         role.setName(ns);
531         role.setNamespaceCode(name);
532         role.setKimTypeId(KimApiServiceLocator.getKimTypeInfoService().findKimTypeByNameAndNamespace(KimConstants.KIM_TYPE_DEFAULT_NAMESPACE, KimConstants.KIM_TYPE_DEFAULT_NAME).getId());
533         Role customRole = KimApiServiceLocator.getRoleService().createRole(role.build());
534 
535         // create a custom attribute for role qualification
536         Long chartAttributeId = KRADServiceLocator.getSequenceAccessorService().getNextAvailableSequenceNumber("KRIM_ATTR_DEFN_ID_S");
537         KimAttributeBo chartAttribute = new KimAttributeBo();
538         chartAttribute.setId("" + chartAttributeId);
539         chartAttribute.setAttributeName(roleQualifierName);
540         chartAttribute.setComponentName("org.kuali.rice.kim.bo.impl.KimAttributes");
541         chartAttribute.setNamespaceCode("KR-SYS");
542         chartAttribute.setAttributeLabel(roleQualifierName);
543         chartAttribute.setActive(true);
544         KRADServiceLocator.getBusinessObjectService().save(chartAttribute);
545 
546         KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), customRole.getId());
547 
548         List<String> recallCapableRoleIds = KimApiServiceLocator.getPermissionService().getRoleIdsForPermission(recallPerm.getNamespaceCode(), recallPerm.getName());
549         Assert.assertFalse("No recall-capable roles found", recallCapableRoleIds.isEmpty());
550         Assert.assertTrue("New role is not associated with Recall permission", recallCapableRoleIds.contains(customRole.getId()));
551 
552         return customRole;
553     }
554 
555     /**
556      * Assigns user to role with single qualification
557      * @param principalId the principal to assign to role
558      * @param role the role object
559      * @param roleQualifierName the role qualifier name
560      * @param roleQualifierValue the role qualifier value
561      */
562     protected void assignUserQualifiedRole(String principalId, Role role, String roleQualifierName, String roleQualifierValue) {
563         // assign user to role triggered by dynamic, custom role qualifications
564         Map<String, String> qualifications = new HashMap<String, String>();
565         qualifications.put(roleQualifierName, roleQualifierValue);
566         KimApiServiceLocator.getRoleService().assignPrincipalToRole(getPrincipalIdForName("arh14"), role.getNamespaceCode(), role.getName(), qualifications);
567 
568         Collection<String> ids = KimApiServiceLocator.getRoleService().getRoleMemberPrincipalIds(role.getNamespaceCode(), role.getName(), qualifications);
569         Assert.assertTrue("Qualified role assignment failed", ids.contains(principalId));
570     }
571 
572     /**
573      * Tests that an application can customize document type routing authorization via documenttypeauthorizer
574      */
575     @Test public void testRecallWithCustomDocumentTypeAuthorizer() throws Exception {
576         // arh14 is not associated with our doc routing, will be authorized by custom documenttypeauthorizer
577         final String ARH14 = getPrincipalIdForName("arh14");
578 
579         // remove existing initiator recall permission
580         disableInitiatorRecallPermission();
581 
582         RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked = false;
583         RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked = false;
584 
585         // confirm arh14 can't recall doc
586         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("arh14"), RECALL_TEST_DOC, false);
587 
588         final String RECALL_ROLE_NM = "CustomRecall";
589         final String RECALL_ROLE_NS = "KR-SYS";
590 
591         // assign permission triggered by dynamic, custom permission details
592         Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE, null, null);
593         Role recallRole = createRoleWithRecallPermission(RECALL_ROLE_NM, RECALL_ROLE_NS, recallPerm, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME);
594         assignUserQualifiedRole(ARH14, recallRole, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE);
595 
596         Map<String, String> d = new HashMap<String, String>();
597         d.put(RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE);
598         d.put(KewApiConstants.DOCUMENT_TYPE_NAME_DETAIL, RECALL_TEST_DOC);
599         d.put(KewApiConstants.ROUTE_NODE_NAME_DETAIL, ROUTE_NODE);
600         d.put(KewApiConstants.DOCUMENT_STATUS_DETAIL, ROUTE_STATUS);
601         Map<String, String> q = new HashMap<String, String>();
602         q.put(RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_NAME, RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE);
603         // test that arh14 has recall permission via new recall role with proper qualifications
604         List<Permission> permissions = KimApiServiceLocator.getPermissionService().getAuthorizedPermissionsByTemplate(ARH14, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, d, q);
605         Assert.assertEquals(1, permissions.size());
606         Assert.assertEquals(recallPerm.getId(), permissions.get(0).getId());
607 
608         // verify that arh14 *still* can't recall doc - we have to set the custom documenttypeauthorizer first
609         testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false);
610 
611         // now update the doctype with custom documenttype authorizer
612         org.kuali.rice.kew.api.doctype.DocumentType dt = KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(RECALL_TEST_DOC);
613         org.kuali.rice.kew.api.doctype.DocumentType.Builder b = org.kuali.rice.kew.api.doctype.DocumentType.Builder.create(dt);
614         b.setAuthorizer(RecallTestDocumentTypeAuthorizer.class.getName());
615 
616         KEWServiceLocator.getDocumentTypeService().save(DocumentType.from(b));
617 
618         Assert.assertEquals(RecallTestDocumentTypeAuthorizer.class.getName(), KewApiServiceLocator.getDocumentTypeService().getDocumentTypeByName(RECALL_TEST_DOC).getAuthorizer());
619 
620         // custom documenttypeauthorizer has not been invoked yet
621         Assert.assertFalse(RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked);
622         Assert.assertFalse(RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked);
623 
624         // arh14 should *now* be able to recall!
625         testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC);
626 
627         Assert.assertTrue(RecallTestDocumentTypeAuthorizer.buildPermissionDetailsInvoked);
628         Assert.assertTrue(RecallTestDocumentTypeAuthorizer.buildRoleQualifiersInvoked);
629 
630         // final counter tests - change the actual dynamic values to ensure match fails
631         String orig = RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE;
632         try {
633             RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE = "I will not match";
634             testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false);
635         } finally {
636             RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_QUALIFIER_VALUE = orig;
637         }
638 
639         orig = RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE;
640         try {
641             RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE = "I won't match either";
642             testRecallToActionListAfterApprovals(EWESTFAL, ARH14, RECALL_TEST_DOC, false);
643         } finally {
644             RecallTestDocumentTypeAuthorizer.CUSTOM_RECALL_DETAIL_VALUE = orig;
645         }
646     }
647 
648     protected void testRecallToActionListAsInitiatorAfterApprovals(String doctype) {
649         testRecallToActionListAfterApprovals(EWESTFAL, EWESTFAL, doctype);
650     }
651 
652     // Implements various permutations of recalls - with and without doctype policies/notifications of various sorts
653     // and as initiator or a third party recaller
654     protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype) {
655         testRecallToActionListAfterApprovals(initiator, recaller, doctype, true);
656     }
657     protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype, boolean expect_recall_success) {
658         boolean notifyPreviousRecipients = !RECALL_TEST_DOC.equals(doctype);
659         boolean notifyPendingRecipients = !RECALL_NO_PENDING_NOTIFY_TEST_DOC.equals(doctype);
660         String[] thirdPartiesNotified = RECALL_NOTIFY_THIRDPARTY_TEST_DOC.equals(doctype) ? new String[] { "quickstart", "admin" } : new String[] {};
661         
662         WorkflowDocument document = WorkflowDocumentFactory.createDocument(initiator, doctype);
663         document.route("");
664 
665         WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve("");
666         WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).approve("");
667         WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).approve("");
668 
669         document = WorkflowDocumentFactory.loadDocument(recaller, document.getDocumentId());
670         System.err.println(document.getValidActions().getValidActions());
671         if (expect_recall_success) {
672             assertTrue("recaller '" + recaller + "' should be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL));
673         } else {
674             assertFalse("recaller '" + recaller + "' should NOT be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL));
675             return;
676         }
677         document.recall("recalling", false);
678 
679         assertTrue("Document should be saved", document.isSaved());
680         assertAfterActionTakenCalled(ActionType.RECALL, ActionType.COMPLETE);
681 
682         // the recaller has a completion request
683         assertTrue(document.isCompletionRequested());
684         
685         // pending approver has FYI
686         assertEquals(notifyPendingRecipients, WorkflowDocumentFactory.loadDocument(NATJOHNS, document.getDocumentId()).isFYIRequested());
687         // third approver has FYI
688         assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).isFYIRequested());
689         // second approver does not have FYI - approver is initiator, FYI is skipped
690         assertFalse(WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).isFYIRequested());
691         // first approver has FYI
692         assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested());
693 
694         if (!ArrayUtils.isEmpty(thirdPartiesNotified)) {
695             for (String recipient: thirdPartiesNotified) {
696                 assertTrue("Expected FYI to be sent to: " + recipient, WorkflowDocumentFactory.loadDocument(getPrincipalIdForName(recipient), document.getDocumentId()).isFYIRequested());
697             }
698         }
699         
700         // omit JHOPF, and see if FYI is subsumed by approval request
701         for (String user: new String[] { RKIRKEND, NATJOHNS }) {
702             WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()).fyi();
703         }
704 
705         document.complete("completing");
706 
707         assertTrue("Document should be enroute", document.isEnroute());
708 
709         // generation of approval requests nullify FYIs (?)
710         // if JHOPF had an FYI, he doesn't any longer
711         for (String user: new String[] { JHOPF, RKIRKEND, NATJOHNS }) {
712             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
713             assertFalse(getPrincipalNameForId(user) + " should not have an FYI", document.isFYIRequested());
714         }
715 
716         // submit all approvals
717         for (String user: new String[] { JHOPF, initiator, RKIRKEND, NATJOHNS, BMCGOUGH }) {
718             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
719             assertTrue(getPrincipalNameForId(user) + " should have approval request", document.isApprovalRequested());
720             document.approve("approving");
721         }
722 
723         // 2 acks outstanding, we're PROCESSED
724         assertTrue("Document should be processed", document.isProcessed());
725         assertTrue("Document should be approved", document.isApproved());
726 
727         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId());
728         document.acknowledge("");
729 
730         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId());
731         document.fyi();
732 
733         assertTrue("Document should be approved", document.isApproved());
734         assertTrue("Document should be final", document.isFinal());
735     }
736 }