001    /*
002     * Copyright 2006-2012 The Kuali Foundation
003     *
004     * Licensed under the Educational Community License, Version 2.0 (the "License");
005     * you may not use this file except in compliance with the License.
006     * You may obtain a copy of the License at
007     *
008     * http://www.opensource.org/licenses/ecl2.php
009     *
010     * Unless required by applicable law or agreed to in writing, software
011     * distributed under the License is distributed on an "AS IS" BASIS,
012     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     * See the License for the specific language governing permissions and
014     * limitations under the License.
015     */
016    package org.kuali.rice.kew.actions;
017    
018    import junit.framework.Assert;
019    import org.apache.commons.lang.ArrayUtils;
020    import org.junit.Test;
021    import org.kuali.rice.coreservice.api.parameter.Parameter;
022    import org.kuali.rice.coreservice.framework.CoreFrameworkServiceLocator;
023    import org.kuali.rice.kew.actionitem.ActionItem;
024    import org.kuali.rice.kew.api.KewApiConstants;
025    import org.kuali.rice.kew.api.KewApiServiceLocator;
026    import org.kuali.rice.kew.api.WorkflowDocument;
027    import org.kuali.rice.kew.api.WorkflowDocumentFactory;
028    import org.kuali.rice.kew.api.action.ActionRequest;
029    import org.kuali.rice.kew.api.action.ActionRequestType;
030    import org.kuali.rice.kew.api.action.ActionType;
031    import org.kuali.rice.kew.api.action.InvalidActionTakenException;
032    import org.kuali.rice.kew.doctype.bo.DocumentType;
033    import org.kuali.rice.kew.doctype.service.impl.KimDocumentTypeAuthorizer;
034    import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
035    import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
036    import org.kuali.rice.kew.postprocessor.DefaultPostProcessor;
037    import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
038    import org.kuali.rice.kew.service.KEWServiceLocator;
039    import org.kuali.rice.kew.test.KEWTestCase;
040    import org.kuali.rice.kim.api.KimConstants;
041    import org.kuali.rice.kim.api.common.template.Template;
042    import org.kuali.rice.kim.api.permission.Permission;
043    import org.kuali.rice.kim.api.role.Role;
044    import org.kuali.rice.kim.api.services.KimApiServiceLocator;
045    import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo;
046    import org.kuali.rice.krad.service.KRADServiceLocator;
047    import org.kuali.rice.krad.util.KRADConstants;
048    
049    import java.util.Collection;
050    import java.util.HashMap;
051    import java.util.List;
052    import java.util.Map;
053    
054    import static org.junit.Assert.*;
055    
056    public class RecallActionTest extends KEWTestCase {
057        /**
058         * test postprocessor for testing afterActionTaken hook
059         */
060        public static class RecallTestPostProcessor extends DefaultPostProcessor {
061            public static ActionType afterActionTakenType;
062            public static ActionTakenEvent afterActionTakenEvent;
063            @Override
064            public ProcessDocReport afterActionTaken(ActionType performed, ActionTakenEvent event) throws Exception {
065                afterActionTakenType = performed;
066                afterActionTakenEvent = event;
067                return super.afterActionTaken(performed, event);
068            }
069        }
070    
071        public static class RecallTestDocumentTypeAuthorizer extends KimDocumentTypeAuthorizer {
072            public static String CUSTOM_RECALL_QUALIFIER_NAME = "Dynamic Qualifier";
073            public static String CUSTOM_RECALL_QUALIFIER_VALUE = "Dynamic Qualifier Value";
074            // we have to use a detail already defined for the recall permission - app doc status seems the most application-controlled
075            public static String CUSTOM_RECALL_DETAIL_NAME = KimConstants.AttributeConstants.APP_DOC_STATUS;
076            public static String CUSTOM_RECALL_DETAIL_VALUE = "Dynamic Recall Permission Detail Value";
077    
078            public static boolean buildPermissionDetailsInvoked = false;
079            public static boolean buildRoleQualifiersInvoked= false;
080    
081            @Override
082            protected Map<String, String> buildDocumentTypePermissionDetails(DocumentType documentType, String documentStatus, String actionRequestedCode, String routeNodeName) {
083                buildPermissionDetailsInvoked = true;
084                Map<String, String> details = super.buildDocumentTypePermissionDetails(documentType, documentStatus, actionRequestedCode, routeNodeName);
085                details.put(CUSTOM_RECALL_DETAIL_NAME, CUSTOM_RECALL_DETAIL_VALUE);
086                return details;
087            }
088    
089            @Override
090            protected Map<String, String> buildDocumentRoleQualifiers(DocumentRouteHeaderValue document, String routeNodeName) {
091                buildRoleQualifiersInvoked = true;
092                Map<String, String> qualifiers = super.buildDocumentRoleQualifiers(document, routeNodeName);
093                qualifiers.put(CUSTOM_RECALL_QUALIFIER_NAME, CUSTOM_RECALL_QUALIFIER_VALUE);
094                return qualifiers;
095            }
096        }
097    
098        private static final String RECALL_TEST_DOC = "RecallTest";
099        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    }