View Javadoc

1   /*
2    * Copyright 2006-2012 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.actions;
17  
18  import org.apache.commons.collections.CollectionUtils;
19  import org.apache.commons.lang.ArrayUtils;
20  import org.junit.Ignore;
21  import org.junit.Test;
22  import org.kuali.rice.core.api.criteria.QueryByCriteria;
23  import org.kuali.rice.kew.actionitem.ActionItem;
24  import org.kuali.rice.kew.actions.BlanketApproveTest.NotifySetup;
25  import org.kuali.rice.kew.api.KewApiConstants;
26  import org.kuali.rice.kew.api.WorkflowDocument;
27  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
28  import org.kuali.rice.kew.api.action.ActionRequest;
29  import org.kuali.rice.kew.api.action.ActionType;
30  import org.kuali.rice.kew.api.action.InvalidActionTakenException;
31  import org.kuali.rice.kew.service.KEWServiceLocator;
32  import org.kuali.rice.kew.test.KEWTestCase;
33  import org.kuali.rice.kim.api.KimConstants;
34  import org.kuali.rice.kim.api.common.template.Template;
35  import org.kuali.rice.kim.api.permission.Permission;
36  import org.kuali.rice.kim.api.role.Role;
37  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
38  import org.kuali.rice.kim.api.type.KimType;
39  import org.kuali.rice.kim.api.type.KimTypeAttribute;
40  import org.kuali.rice.kim.impl.common.attribute.KimAttributeBo;
41  import org.kuali.rice.kim.impl.permission.PermissionBo;
42  import org.kuali.rice.kim.impl.permission.PermissionTemplateBo;
43  import org.kuali.rice.kim.impl.responsibility.ResponsibilityTemplateBo;
44  import org.kuali.rice.kim.impl.services.KimImplServiceLocator;
45  import org.kuali.rice.kim.impl.type.KimTypeAttributeBo;
46  import org.kuali.rice.kim.impl.type.KimTypeBo;
47  import org.kuali.rice.krad.bo.Note;
48  import org.kuali.rice.krad.service.KRADServiceLocator;
49  import org.kuali.rice.krad.service.KRADServiceLocatorWeb;
50  
51  import java.util.Collection;
52  import java.util.HashMap;
53  import java.util.List;
54  import java.util.Map;
55  
56  import static org.junit.Assert.*;
57  import static org.junit.Assert.assertEquals;
58  
59  public class RecallActionTest extends KEWTestCase {
60      private static final String RECALL_TEST_DOC = "RecallTest";
61      private static final String RECALL_NOTIFY_TEST_DOC = "RecallWithPrevNotifyTest";
62      private static final String RECALL_NO_PENDING_NOTIFY_TEST_DOC = "RecallWithoutPendingNotifyTest";
63      private static final String RECALL_NOTIFY_THIRDPARTY_TEST_DOC = "RecallWithThirdPartyNotifyTest";
64  
65      private String EWESTFAL = null;
66      private String JHOPF = null;
67      private String RKIRKEND = null;
68      private String NATJOHNS = null;
69      private String BMCGOUGH = null;
70  
71      protected void loadTestData() throws Exception {
72          loadXmlFile("ActionsConfig.xml");
73      }
74  
75      @Override
76      protected void setUpAfterDataLoad() throws Exception {
77          super.setUpAfterDataLoad();
78          EWESTFAL = getPrincipalIdForName("ewestfal");
79          JHOPF = getPrincipalIdForName("jhopf");
80          RKIRKEND = getPrincipalIdForName("rkirkend");
81          NATJOHNS = getPrincipalIdForName("natjohns");
82          BMCGOUGH = getPrincipalIdForName("bmcgough");
83      }
84  
85      @Test(expected=InvalidActionTakenException.class) public void testCantRecallUnroutedDoc() {
86          WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
87          document.recall("recalling", true);
88      }
89  
90      @Test public void testRecallAsInitiatorBeforeAnyApprovals() throws Exception {
91          WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
92          document.route("");
93  
94          document.recall("recalling", true);
95  
96          assertTrue("Document should be recalled", document.isRecalled());
97  
98          //verify that the document is truly dead - no more action requests or action items.
99  
100         List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId());
101         assertEquals("Should not have any active requests", 0, requests.size());
102 
103         Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId());
104         assertEquals("Should not have any action items", 0, actionItems.size());
105     }
106 
107     @Test public void testRecallAsInitiatorAfterSingleApproval() throws Exception {
108         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
109         document.route("");
110 
111         document = WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId());
112         document.approve("");
113 
114         document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId());
115         document.recall("recalling", true);
116 
117         assertTrue("Document should be recalled", document.isRecalled());
118 
119         //verify that the document is truly dead - no more action requests or action items.
120 
121         List requests = KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId());
122         assertEquals("Should not have any active requests", 0, requests.size());
123 
124         Collection<ActionItem> actionItems = KEWServiceLocator.getActionListService().findByDocumentId(document.getDocumentId());
125         assertEquals("Should not have any action items", 0, actionItems.size());
126 
127         // can't recall recalled doc
128         assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL));
129     }
130 
131     @Test(expected=InvalidActionTakenException.class)
132     public void testRecallInvalidWhenProcessed() throws Exception {
133         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
134         document.route("");
135 
136         for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) {
137             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
138             document.approve("");
139         }
140 
141         document.refresh();
142         assertTrue("Document should be processed", document.isProcessed());
143         assertTrue("Document should be approved", document.isApproved());
144         assertFalse("Document should not be final", document.isFinal());
145 
146         document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId());
147         document.recall("recalling when processed should fail", true);
148     }
149 
150     @Test(expected=InvalidActionTakenException.class)
151     public void testRecallInvalidWhenFinal() throws Exception {
152         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
153         document.route("");
154 
155         for (String user: new String[] { JHOPF, EWESTFAL, RKIRKEND, NATJOHNS, BMCGOUGH }) {
156             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
157             document.approve("");
158         }
159         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId());
160         document.acknowledge("");
161 
162         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId());
163         document.fyi();
164 
165         for (ActionRequest a: document.getRootActionRequests()) {
166             System.err.println(a);
167             if (a.isAcknowledgeRequest() || a.isFyiRequest()) {
168                 System.err.println(a.getPrincipalId());
169                 System.err.println(KimApiServiceLocator.getIdentityService().getPrincipal(a.getPrincipalId()).getPrincipalName());
170             }
171         }
172 
173         assertFalse("Document should not be processed", document.isProcessed());
174         assertTrue("Document should be approved", document.isApproved());
175         assertTrue("Document should be final", document.isFinal());
176 
177         document = WorkflowDocumentFactory.loadDocument(EWESTFAL, document.getDocumentId());
178         document.recall("recalling when processed should fail", true);
179     }
180 
181     @Test public void testRecallToActionListAsInitiatorBeforeAnyApprovals() throws Exception {
182         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
183         document.route("");
184 
185         document.recall("recalling", false);
186 
187         assertTrue("Document should be saved", document.isSaved());
188         assertEquals(1, document.getCurrentNodeNames().size());
189         assertTrue(document.getCurrentNodeNames().contains("AdHoc"));
190 
191         // initiator has completion request
192         assertTrue(document.isCompletionRequested());
193         // can't recall saved doc
194         assertFalse(document.getValidActions().getValidActions().contains(ActionType.RECALL));
195 
196         // first approver has FYI
197         assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested());
198 
199         document.complete("completing");
200 
201         assertTrue("Document should be enroute", document.isEnroute());
202 
203         assertTrue(WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isApprovalRequested());
204     }
205 
206     private static final String PERM_APP_DOC_STATUS = "recallable by admins";
207     private static final String ROUTE_NODE = "NotifyFirst";
208     private static final String ROUTE_STATUS = "R";
209 
210     protected Permission createRecallPermission(String docType, String appDocStatus, String routeNode, String routeStatus) {
211         return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus);
212     }
213 
214     protected Permission createRouteDocumentPermission(String docType, String appDocStatus, String routeNode, String routeStatus) {
215         return createPermissionForTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION, KewApiConstants.KEW_NAMESPACE, KewApiConstants.ROUTE_PERMISSION + " for test case", docType, appDocStatus, routeNode, routeStatus);
216     }
217 
218     protected Permission createPermissionForTemplate(String template_ns, String template_name, String permission_ns, String permission_name, String docType, String appDocStatus, String routeNode, String routeStatus) {
219         Template permTmpl = KimApiServiceLocator.getPermissionService().findPermTemplateByNamespaceCodeAndName(template_ns, template_name);
220         assertNotNull(permTmpl);
221         Permission.Builder permission = Permission.Builder.create(permission_ns, permission_name);
222         permission.setDescription(permission_name);
223         permission.setTemplate(Template.Builder.create(permTmpl));
224         Map<String, String> attrs = new HashMap<String, String>();
225         attrs.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, docType);
226         attrs.put(KimConstants.AttributeConstants.APP_DOC_STATUS, appDocStatus);
227         attrs.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, routeNode);
228         attrs.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, routeStatus);
229         permission.setActive(true);
230         permission.setAttributes(attrs);
231 
232         // save the permission and check that's it's wired up correctly
233         Permission perm = KimApiServiceLocator.getPermissionService().createPermission(permission.build());
234         assertEquals(perm.getTemplate().getId(), permTmpl.getId());
235         int num = 1;
236         if (appDocStatus != null) num++;
237         if (routeNode != null) num++;
238         if (routeStatus != null) num++;
239         assertEquals(num, perm.getAttributes().size());
240         assertEquals(docType, perm.getAttributes().get(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME));
241         assertEquals(appDocStatus, perm.getAttributes().get(KimConstants.AttributeConstants.APP_DOC_STATUS));
242         assertEquals(routeNode, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_NODE_NAME));
243         assertEquals(routeStatus, perm.getAttributes().get(KimConstants.AttributeConstants.ROUTE_STATUS_CODE));
244 
245         return perm;
246     }
247 
248     // disable the existing Recall Permission assigned to Initiator Role for test purposes
249     protected void disableInitiatorRecallPermission() {
250         Permission p = KimApiServiceLocator.getPermissionService().findPermByNamespaceCodeAndName("KR-WKFLW", "Recall Document");
251         Permission.Builder pb = Permission.Builder.create(p);
252         pb.setActive(false);
253         KimApiServiceLocator.getPermissionService().updatePermission(pb.build());
254     }
255 
256     /**
257      * Tests that a new permission can be configured with the Recall Permission template and that matching works correctly
258      * against the new permission
259      */
260     @Test public void testRecallPermissionMatching() {
261         disableInitiatorRecallPermission();
262         createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS);
263 
264         Map<String, String> details = new HashMap<String, String>();
265         details.put(KimConstants.AttributeConstants.DOCUMENT_TYPE_NAME, RECALL_TEST_DOC);
266         details.put(KimConstants.AttributeConstants.APP_DOC_STATUS, PERM_APP_DOC_STATUS);
267         details.put(KimConstants.AttributeConstants.ROUTE_NODE_NAME, ROUTE_NODE);
268         details.put(KimConstants.AttributeConstants.ROUTE_STATUS_CODE, ROUTE_STATUS);
269 
270         // test all single field mismatches
271         for (Map.Entry<String, String> entry: details.entrySet()) {
272             Map<String, String> testDetails = new HashMap<String, String>(details);
273             // change a single detail to a non-matching value
274             testDetails.put(entry.getKey(), entry.getValue() + " BOGUS ");
275             assertFalse("non-matching " + entry.getKey() + " detail should cause template to not match", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, testDetails));
276         }
277 
278         assertTrue("template should match details", KimApiServiceLocator.getPermissionService().isPermissionDefinedByTemplate(KewApiConstants.KEW_NAMESPACE, KewApiConstants.RECALL_PERMISSION, details));
279     }
280 
281     @Test public void testRecallPermissionTemplate() throws Exception {
282         WorkflowDocument document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
283         document.route("");
284 
285         // nope, technical admins can't recall
286         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
287         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
288 
289         // create a recall permission for the RECALL_TEST_DOC doctype
290         Permission perm = createRecallPermission(RECALL_TEST_DOC, PERM_APP_DOC_STATUS, ROUTE_NODE, ROUTE_STATUS);
291 
292         // assign the permission to Technical Administrator role
293         Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator");
294         KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId());
295 
296         // our recall permission is assigned to the technical admin role
297 
298         // but the doc will not match...
299         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_NOTIFY_TEST_DOC);
300         document.route(PERM_APP_DOC_STATUS);
301         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
302         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
303 
304         // .. the app doc status will not match...
305         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
306         document.route("");
307         // technical admins can't recall since the app doc status is not correct
308         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
309         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
310 
311         // ... the node will not match ...
312         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
313         document.route("");
314         WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve(""); // approve past notifyfirstnode
315         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
316         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
317 
318         // ... the doc status will not match (not recallable anyway) ...
319         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
320         document.route("");
321         document.cancel("cancelled");
322         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
323         assertFalse(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
324 
325         // everything should match
326         document = WorkflowDocumentFactory.createDocument(EWESTFAL, RECALL_TEST_DOC);
327         document.setApplicationDocumentStatus(PERM_APP_DOC_STATUS);
328         document.route("");
329         // now technical admins can recall by virtue of having the recall permission on this doc
330         assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("admin"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
331         assertTrue(WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("quickstart"), document.getDocumentId()).getValidActions().getValidActions().contains(ActionType.RECALL));
332     }
333 
334     @Test public void testRecallToActionListAsInitiatorAfterApprovals() throws Exception {
335         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_TEST_DOC);
336     }
337 
338     @Test public void testRecallToActionListAsInitiatorWithNotificationAfterApprovals() throws Exception {
339         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_TEST_DOC);
340     }
341 
342     @Test public void testRecallToActionListAsInitiatorWithoutPendingNotificationAfterApprovals() throws Exception {
343         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NO_PENDING_NOTIFY_TEST_DOC);
344     }
345 
346     @Test public void testRecallToActionListAsInitiatorWithThirdPartyNotificationAfterApprovals() throws Exception {
347         this.testRecallToActionListAsInitiatorAfterApprovals(RECALL_NOTIFY_THIRDPARTY_TEST_DOC);
348     }
349 
350     /**
351      * Tests that the document is returned to the *recaller*'s action list, not the original initiator
352      * @throws Exception
353      */
354     @Test public void testRecallToActionListAsThirdParty() throws Exception {
355         Permission perm = createRecallPermission(RECALL_TEST_DOC, null, null, null);
356         // assign the permission to Technical Administrator role
357         Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator");
358         KimApiServiceLocator.getRoleService().assignPermissionToRole(perm.getId(), techadmin.getId());
359         // recall as 'admin' user
360         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC);
361     }
362 
363     // the three tests below test permutations of recall permission and derived role assignment
364     protected void assignRoutePermissionToTechAdmin() {
365         // assign Route Document permission to the Technical Administrator role
366         Permission routePerm = createRouteDocumentPermission(RECALL_TEST_DOC, null, null, null);
367         Role techadmin = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-SYS", "Technical Administrator");
368         KimApiServiceLocator.getRoleService().assignPermissionToRole(routePerm.getId(), techadmin.getId());
369     }
370     protected void assignRecallPermissionToDocumentRouters() {
371         // assign Recall permission to the Document Router derived role
372         Permission recallPerm = createRecallPermission(RECALL_TEST_DOC, null, null, null);
373         Role documentRouterDerivedRole = KimApiServiceLocator.getRoleService().getRoleByNamespaceCodeAndName("KR-WKFLW", "Document Router");
374         KimApiServiceLocator.getRoleService().assignPermissionToRole(recallPerm.getId(), documentRouterDerivedRole.getId());
375     }
376     /**
377      * Tests that simply assigning the Route Document permission to the Technical Admin role *without* assigning the
378      * Recall permission to the Document Router derived role, is NOT sufficient to enable recall.
379      */
380     @Test public void testRoutePermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception {
381         assignRoutePermissionToTechAdmin();
382         // recall as 'admin' (Tech Admin) user
383         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false);
384     }
385     /**
386      * Tests that simply assigning the recall permission to the Document Router derived role *without* assigning the
387      * Route Document permission to the Technical Admin role, is NOT sufficient to enable recall.
388      */
389     @Test public void testRecallPermissionAssignmentInsufficientForRouterToRecallDoc() throws Exception {
390         assignRecallPermissionToDocumentRouters();
391         // recall as 'admin' (Tech Admin) user
392         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC, false);
393     }
394     /**
395      * Tests that we can use the Route Document derived role to assign Recall permission to document routers.
396      */
397     @Test public void testRecallToActionListAsRouterDerivedRole() throws Exception {
398         // assign both! derived role works its magic
399         assignRoutePermissionToTechAdmin();
400         assignRecallPermissionToDocumentRouters();
401         // recall as 'admin' user (Tech Admin) user
402         testRecallToActionListAfterApprovals(EWESTFAL, getPrincipalIdForName("admin"), RECALL_TEST_DOC);
403     }
404 
405     protected void testRecallToActionListAsInitiatorAfterApprovals(String doctype) {
406         testRecallToActionListAfterApprovals(EWESTFAL, EWESTFAL, doctype);
407     }
408 
409     // Implements various permutations of recalls - with and without doctype policies/notifications of various sorts
410     // and as initiator or a third party recaller
411     protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype) {
412         testRecallToActionListAfterApprovals(initiator, recaller, doctype, true);
413     }
414     protected void testRecallToActionListAfterApprovals(String initiator, String recaller, String doctype, boolean expect_recall_success) {
415         boolean notifyPreviousRecipients = !RECALL_TEST_DOC.equals(doctype);
416         boolean notifyPendingRecipients = !RECALL_NO_PENDING_NOTIFY_TEST_DOC.equals(doctype);
417         String[] thirdPartiesNotified = RECALL_NOTIFY_THIRDPARTY_TEST_DOC.equals(doctype) ? new String[] { "quickstart", "admin" } : new String[] {};
418         
419         WorkflowDocument document = WorkflowDocumentFactory.createDocument(initiator, doctype);
420         document.route("");
421 
422         WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).approve("");
423         WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).approve("");
424         WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).approve("");
425 
426         document = WorkflowDocumentFactory.loadDocument(recaller, document.getDocumentId());
427         System.err.println(document.getValidActions().getValidActions());
428         if (expect_recall_success) {
429             assertTrue("recaller '" + recaller + "' should be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL));
430         } else {
431             assertFalse("recaller '" + recaller + "' should NOT be able to RECALL", document.getValidActions().getValidActions().contains(ActionType.RECALL));
432             return;
433         }
434         document.recall("recalling", false);
435 
436         assertTrue("Document should be saved", document.isSaved());
437 
438         // the recaller has a completion request
439         assertTrue(document.isCompletionRequested());
440         
441         // pending approver has FYI
442         assertEquals(notifyPendingRecipients, WorkflowDocumentFactory.loadDocument(NATJOHNS, document.getDocumentId()).isFYIRequested());
443         // third approver has FYI
444         assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(RKIRKEND, document.getDocumentId()).isFYIRequested());
445         // second approver does not have FYI - approver is initiator, FYI is skipped
446         assertFalse(WorkflowDocumentFactory.loadDocument(initiator, document.getDocumentId()).isFYIRequested());
447         // first approver has FYI
448         assertEquals(notifyPreviousRecipients, WorkflowDocumentFactory.loadDocument(JHOPF, document.getDocumentId()).isFYIRequested());
449 
450         if (!ArrayUtils.isEmpty(thirdPartiesNotified)) {
451             for (String recipient: thirdPartiesNotified) {
452                 assertTrue("Expected FYI to be sent to: " + recipient, WorkflowDocumentFactory.loadDocument(getPrincipalIdForName(recipient), document.getDocumentId()).isFYIRequested());
453             }
454         }
455         
456         // omit JHOPF, and see if FYI is subsumed by approval request
457         for (String user: new String[] { RKIRKEND, NATJOHNS }) {
458             WorkflowDocumentFactory.loadDocument(user, document.getDocumentId()).fyi();
459         }
460 
461         document.complete("completing");
462 
463         assertTrue("Document should be enroute", document.isEnroute());
464 
465         // generation of approval requests nullify FYIs (?)
466         // if JHOPF had an FYI, he doesn't any longer
467         for (String user: new String[] { JHOPF, RKIRKEND, NATJOHNS }) {
468             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
469             assertFalse(getPrincipalNameForId(user) + " should not have an FYI", document.isFYIRequested());
470         }
471 
472         // submit all approvals
473         for (String user: new String[] { JHOPF, initiator, RKIRKEND, NATJOHNS, BMCGOUGH }) {
474             document = WorkflowDocumentFactory.loadDocument(user, document.getDocumentId());
475             assertTrue(getPrincipalNameForId(user) + " should have approval request", document.isApprovalRequested());
476             document.approve("approving");
477         }
478 
479         // 2 acks outstanding, we're PROCESSED
480         assertTrue("Document should be processed", document.isProcessed());
481         assertTrue("Document should be approved", document.isApproved());
482 
483         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("xqi"), document.getDocumentId());
484         document.acknowledge("");
485 
486         document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("jthomas"), document.getDocumentId());
487         document.fyi();
488 
489         assertTrue("Document should be approved", document.isApproved());
490         assertTrue("Document should be final", document.isFinal());
491     }
492 }