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