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