View Javadoc

1   /**
2    * Copyright 2005-2013 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.test;
17  
18  import org.apache.commons.io.FileUtils;
19  import org.apache.commons.lang.StringUtils;
20  import org.apache.commons.lang.SystemUtils;
21  import org.junit.Assert;
22  import org.kuali.rice.core.api.config.property.Config;
23  import org.kuali.rice.core.api.config.property.ConfigContext;
24  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
25  import org.kuali.rice.kew.actionitem.ActionItem;
26  import org.kuali.rice.kew.actionrequest.ActionRequestValue;
27  import org.kuali.rice.kew.api.WorkflowDocument;
28  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
29  import org.kuali.rice.kew.api.exception.WorkflowException;
30  import org.kuali.rice.kew.engine.node.RouteNodeInstance;
31  import org.kuali.rice.kew.service.KEWServiceLocator;
32  import org.kuali.rice.kim.api.identity.IdentityService;
33  import org.kuali.rice.kim.api.identity.principal.Principal;
34  import org.kuali.rice.kim.api.services.KimApiServiceLocator;
35  import org.springframework.jdbc.core.ConnectionCallback;
36  import org.springframework.jdbc.core.JdbcTemplate;
37  import org.springframework.jdbc.core.StatementCallback;
38  import org.springframework.transaction.PlatformTransactionManager;
39  import org.springframework.transaction.TransactionStatus;
40  import org.springframework.transaction.support.TransactionCallback;
41  import org.springframework.transaction.support.TransactionTemplate;
42  
43  import javax.sql.DataSource;
44  import java.io.File;
45  import java.io.FileInputStream;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.sql.Connection;
49  import java.sql.ResultSet;
50  import java.sql.SQLException;
51  import java.sql.Statement;
52  import java.util.ArrayList;
53  import java.util.Collection;
54  import java.util.HashSet;
55  import java.util.Iterator;
56  import java.util.List;
57  import java.util.Properties;
58  import java.util.Set;
59  
60  import static org.junit.Assert.fail;
61  
62  
63  /**
64   * Defines utilities for unit testing
65   */
66  public final class TestUtilities {
67  
68      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(TestUtilities.class);
69  
70      private static final String TEST_TABLE_NAME = "EN_UNITTEST_T";
71      private static Thread exceptionThreader;
72  
73  	private TestUtilities() {
74  		throw new UnsupportedOperationException("do not call");
75  	}
76  
77      public static InputStream loadResource(Class packageClass, String resourceName) {
78      	return packageClass.getResourceAsStream(resourceName);
79      }
80  
81      public static TransactionTemplate getTransactionTemplate() {
82  		return (TransactionTemplate)
83  					GlobalResourceLoader.getService(KEWServiceLocator.TRANSACTION_TEMPLATE);
84  	}
85  
86      public static void verifyTestEnvironment(DataSource dataSource) {
87          if (dataSource == null) {
88              Assert.fail("Could not locate the data source.");
89          }
90          JdbcTemplate template = new JdbcTemplate(dataSource);
91          template.execute(new ConnectionCallback() {
92              public Object doInConnection(Connection connection) throws SQLException {
93                  ResultSet resultSet = connection.getMetaData().getTables(null, null, TEST_TABLE_NAME, null);
94                  if (!resultSet.next()) {
95                      LOG.error("No table named '"+TEST_TABLE_NAME+"' was found in the configured database.  " +
96                              "You are attempting to run tests against a non-test database!!!");
97                      LOG.error("The test environment will not start up properly!!!");
98                      Assert.fail("No table named '"+TEST_TABLE_NAME+"' was found in the configured database.  " +
99                      		"You are attempting to run tests against a non-test database!!!");
100                 }
101                 return null;
102             }
103         });
104     }
105 
106     public static void clearTables(final PlatformTransactionManager transactionManager, final DataSource dataSource, final String edenSchemaName, final List<String> dontClear) {
107         LOG.info("Clearing tables for schema " + edenSchemaName);
108         if (dataSource == null) {
109             Assert.fail("Null data source given");
110         }
111         if (edenSchemaName == null || edenSchemaName.equals("")) {
112             Assert.fail("Empty eden schema name given");
113         }
114         new TransactionTemplate(transactionManager).execute(new TransactionCallback() {
115             public Object doInTransaction(TransactionStatus status) {
116                 verifyTestEnvironment(dataSource);
117                 JdbcTemplate template = new JdbcTemplate(dataSource);
118                 return template.execute(new StatementCallback() {
119                     public Object doInStatement(Statement statement) throws SQLException {
120                         List<String> reEnableConstraints = new ArrayList<String>();
121                     	ResultSet resultSet = statement.getConnection().getMetaData().getTables(null, edenSchemaName, null, new String[] { "TABLE" });
122                         while (resultSet.next()) {
123                             String tableName = resultSet.getString("TABLE_NAME");
124                             if (tableName.startsWith("EN_") && !dontClear.contains(tableName)) {
125                             	ResultSet keyResultSet = statement.getConnection().getMetaData().getExportedKeys(null, edenSchemaName, tableName);
126                             	while (keyResultSet.next()) {
127                             		String fkName = keyResultSet.getString("FK_NAME");
128                             		String fkTableName = keyResultSet.getString("FKTABLE_NAME");
129                             		statement.addBatch("ALTER TABLE "+fkTableName+" DISABLE CONSTRAINT "+fkName);
130                             		reEnableConstraints.add("ALTER TABLE "+fkTableName+" ENABLE CONSTRAINT "+fkName);
131                             	}
132                             	keyResultSet.close();
133                             	statement.addBatch("DELETE FROM "+tableName);
134                             }
135                         }
136                         for (String constraint : reEnableConstraints) {
137                     		statement.addBatch(constraint);
138                     	}
139                         statement.executeBatch();
140                         resultSet.close();
141                         return null;
142                     }
143                 });
144             }
145         });
146         LOG.info("Tables successfully cleared for schema " + edenSchemaName);
147     }
148 
149     public static Set<String> createNodeInstanceNameSet(Collection nodeInstances) {
150     	Set<String> nameSet = new HashSet<String>();
151     	for (Iterator iterator = nodeInstances.iterator(); iterator.hasNext(); ) {
152 			RouteNodeInstance nodeInstance = (RouteNodeInstance) iterator.next();
153 			nameSet.add(nodeInstance.getName());
154 		}
155     	return nameSet;
156     }
157 
158     /**
159      * Checks that the document is at a node with the given name.  This does not check that the document is at
160      * the given node and only the given node, the document can be at other nodes as well and this assertion
161      * will still pass.
162      */
163     public static void assertAtNode(String message, WorkflowDocument document, String nodeNameToAssert) {
164 		Set<String> nodeNames = document.getNodeNames();
165 		for (String nodeName : nodeNames) {
166 			if (nodeNameToAssert.equals(nodeName)) {
167 				return;
168 			}
169 		}
170 		fail((org.apache.commons.lang.StringUtils.isEmpty(message) ? "" : message + ": ") + "Was [" + StringUtils.join(nodeNames, ", ") + "], Expected " + nodeNameToAssert);
171 	}
172 
173     public static void assertAtNode(WorkflowDocument document, String nodeName) throws WorkflowException {
174     	assertAtNode("", document, nodeName);
175     }
176     
177     /**
178      * Checks that the document is at a node with the given name.  This does not check that the document is at
179      * the given node and only the given node, the document can be at other nodes as well and this assertion
180      * will still pass.
181      */
182     public static void assertAtNodeNew(String message, org.kuali.rice.kew.api.WorkflowDocument document, String nodeName) throws WorkflowException {
183 		Set<String> nodeNames = document.getNodeNames();
184 		if (!nodeNames.contains(nodeName)) {
185 			fail((org.apache.commons.lang.StringUtils.isEmpty(message) ? "" : message + ": ") + "Was [" + StringUtils.join(nodeNames, ", ") + "], Expected " + nodeName);
186 		}
187 	}
188     
189     public static void assertAtNodeNew(org.kuali.rice.kew.api.WorkflowDocument document, String nodeName) throws WorkflowException {
190     	assertAtNodeNew("", document, nodeName);
191     }
192 
193     /**
194      * Asserts that the given document id is in the given user's action list.
195      */
196     public static void assertInActionList(String principalId, String documentId) {
197     	Principal principal = KimApiServiceLocator.getIdentityService().getPrincipal(principalId);
198     	Assert.assertNotNull("Given principal id was invalid: " + principalId, principal);
199     	Collection<ActionItem> actionList = KEWServiceLocator.getActionListService().findByPrincipalId(principalId);
200     	for (Iterator iterator = actionList.iterator(); iterator.hasNext();) {
201 			ActionItem actionItem = (ActionItem) iterator.next();
202 			if (actionItem.getDocumentId().equals(documentId)) {
203 				return;
204 			}
205 		}
206     	Assert.fail("Could not locate an action item in the user's action list for the given document id.");
207     }
208 
209     /**
210      * Asserts that the given document id is NOT in the given user's action list.
211      */
212     public static void assertNotInActionList(String principalId, String documentId) {
213     	Principal principal = KimApiServiceLocator.getIdentityService().getPrincipal(principalId);
214     	Assert.assertNotNull("Given principal id was invalid: " + principalId, principal);
215     	Collection actionList = KEWServiceLocator.getActionListService().findByPrincipalId(principalId);
216     	for (Iterator iterator = actionList.iterator(); iterator.hasNext();) {
217 			ActionItem actionItem = (ActionItem) iterator.next();
218 			if (actionItem.getDocumentId().equals(documentId)) {
219 				Assert.fail("Found an action item in the user's acton list for the given document id.");
220 			}
221 		}
222     }
223 
224     public static void assertNumberOfPendingRequests(String documentId, int numberOfPendingRequests) {
225     	List actionRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(documentId);
226     	Assert.assertEquals("Wrong number of pending requests for document: " + documentId, numberOfPendingRequests, actionRequests.size());
227     }
228 
229     /**
230      * Asserts that the user with the given network id has a pending request on the given document
231      */
232     public static void assertUserHasPendingRequest(String documentId, String principalName) throws WorkflowException {
233     	String principalId = KEWServiceLocator.getIdentityHelperService().getIdForPrincipalName(principalName);
234     	List actionRequests = KEWServiceLocator.getActionRequestService().findPendingByDoc(documentId);
235     	boolean foundRequest = false;
236     	for (Iterator iterator = actionRequests.iterator(); iterator.hasNext();) {
237 			ActionRequestValue actionRequest = (ActionRequestValue) iterator.next();
238 			if (actionRequest.isUserRequest() && actionRequest.getPrincipalId().equals(principalId)) {
239 				foundRequest = true;
240 				break;
241 			} else if (actionRequest.isGroupRequest() && 
242 			        KimApiServiceLocator.getGroupService().isMemberOfGroup(principalId, actionRequest.getGroup().getId())) {
243 				foundRequest = true;
244 				break;
245 			}
246 		}
247     	Assert.assertTrue("Could not locate pending request for the given user: " + principalId, foundRequest);
248     }
249 
250     /**
251      * Asserts that the specified users do or do not have outstanding approvals
252      * @param docId the id of the document
253      * @param users the list of users
254      * @param shouldHaveApproval whether they should have an approval outstanding
255      * @throws WorkflowException
256      */
257     public static void assertApprovals(String docId, String[] users, boolean shouldHaveApproval) throws WorkflowException {
258         List<String> failedUsers = new ArrayList<String>();
259         IdentityService ims = KimApiServiceLocator.getIdentityService();
260         for (String user: users) {
261             WorkflowDocument doc = WorkflowDocumentFactory.loadDocument(ims.getPrincipalByPrincipalName(user).getPrincipalId(), docId);
262             boolean appRqsted = doc.isApprovalRequested();
263             if (shouldHaveApproval != appRqsted) {
264                 failedUsers.add(user);
265             }
266             LOG.info("User " + user + (appRqsted ? " HAS " : " HAS NO ") + "approval request");
267         }
268         for (String user: failedUsers) {
269             LOG.error("User " + user + (shouldHaveApproval ? " should have " : " should NOT have ") + " approval");
270         }
271         if (failedUsers.size() > 0) {
272             Assert.fail("Outstanding approvals are incorrect");
273         }
274     }
275     
276     public static WorkflowDocument switchByPrincipalName(String principalName, WorkflowDocument document) throws WorkflowException {
277     	return switchPrincipalId(KEWServiceLocator.getIdentityHelperService().getIdForPrincipalName(principalName), document);
278     }
279     
280     public static WorkflowDocument switchPrincipalId(String principalId, WorkflowDocument document) throws WorkflowException {
281     	return WorkflowDocumentFactory.loadDocument(principalId, document.getDocumentId());
282     }
283 
284     public static void logActionRequests(String docId) {
285         List<ActionRequestValue> actionRequests = KEWServiceLocator.getActionRequestService().findAllActionRequestsByDocumentId(docId);
286         LOG.info("Current action requests:");
287         for (ActionRequestValue ar: actionRequests) {
288             LOG.info(ar);
289         }
290     }
291 
292     public static JdbcTemplate getJdbcTemplate() {
293     	JdbcTemplate jdbcTemplate = new JdbcTemplate(KEWServiceLocator.getDataSource());
294     	jdbcTemplate.afterPropertiesSet();
295     	return jdbcTemplate;
296     }
297     
298     /**
299      * Waits "indefinately" for the exception routing thread to terminate.
300      *
301      * This actually doesn't wait forever but puts an upper bound of 5 minutes
302      * on the time to wait for the exception routing thread to complete.  If a
303      * document cannot go into exception routing within 5 minutes  then we got
304      * problems.
305      */
306     public static void waitForExceptionRouting() {
307     	waitForExceptionRouting(5*60*1000);
308     }
309 
310     public static void waitForExceptionRouting(long milliseconds) {
311     	if (getExceptionThreader() == null) {
312     		return;
313     	}
314     	try {
315     		getExceptionThreader().join(milliseconds);
316     	} catch (InterruptedException e) {
317     		Assert.fail("This thread was interuppted while waiting for exception routing.");
318     	}
319     	if (getExceptionThreader().isAlive()) {
320     		Assert.fail("Document was not put into exception routing within the specified amount of time " + milliseconds);
321     	}
322     }
323 
324     public static Thread getExceptionThreader() {
325         return exceptionThreader;
326     }
327 
328     public static void setExceptionThreader(Thread exceptionThreader) {
329         TestUtilities.exceptionThreader = exceptionThreader;
330     }
331 
332     private static final String DEFAULT_TEST_PLATFORM = "oracle";
333 	private static final String BUILD_PROPERTIES = "build.properties";
334 	private static final String TEST_PLATFORM = "test.platform";
335 
336     /**
337      * Attempts to derive the database "platform" to use for unit tests by
338      * inspected Ant build.properties files in typical locations.
339      * @return the test platform if so defined in Ant build.properties file(s), or the DEFAULT_TEST_PLATFORM otherwise
340      * @see #DEFAULT_TEST_PLATFORM
341      * @throws IOException if anything goes awry
342      */
343 	public static String getTestPlatform() throws IOException {
344         // check the user's build.properties in user's home
345 		File userBuildProperties = new File(SystemUtils.USER_HOME + "/" + BUILD_PROPERTIES);
346 		if (userBuildProperties.isFile()) {
347 			Properties properties = loadProperties(userBuildProperties);
348 			if (properties.containsKey(TEST_PLATFORM)) {
349 				return properties.getProperty(TEST_PLATFORM).toLowerCase();
350 			}
351 		}
352 		// check the "local" build.properties in the current directory
353         File localBuildProperties = new File(BUILD_PROPERTIES);
354         if (localBuildProperties.isFile()) {
355             Properties properties = loadProperties(localBuildProperties);
356             if (properties.containsKey(TEST_PLATFORM)) {
357                 return properties.getProperty(TEST_PLATFORM).toLowerCase();
358             }
359         }
360 		return DEFAULT_TEST_PLATFORM.toLowerCase();
361 	}
362 
363     /**
364      * Loads a file into a Properties object
365      * @param file the file
366      * @return a Properties object
367      */
368     private static Properties loadProperties(File file) throws IOException {
369         Properties properties = new Properties();
370         FileInputStream fis = new FileInputStream(file);
371         try {
372             properties.load(fis);
373         } finally {
374             fis.close();
375         }
376         return properties;
377     }
378 
379 	public static File createTempDir() throws Exception {
380 		File tmpFile = File.createTempFile("wfUnitTest", "");
381 		Assert.assertTrue(tmpFile.delete());
382 		File tmpDir = new File(new File(SystemUtils.JAVA_IO_TMPDIR), tmpFile.getName());
383 		Assert.assertTrue(tmpDir.mkdir());
384 		tmpDir.deleteOnExit();
385 		return tmpDir;
386 }
387 	
388 	public static File getPluginsDirectory() {
389         String directory = ConfigContext.getCurrentContextConfig().getProperty(Config.PLUGIN_DIR);
390         if (StringUtils.isNotBlank(directory)) {
391             return new File(directory);
392         }
393 		return new File("./work/unit-test/plugins");
394 	}
395 
396 	public static void initializePluginDirectories() throws Exception {
397 		File pluginDir = getPluginsDirectory();
398 		if (pluginDir.exists()) {
399 			FileUtils.forceDelete(pluginDir);
400 		}
401 		FileUtils.forceMkdir(pluginDir);
402 		FileUtils.forceDeleteOnExit(pluginDir);
403 	}
404 
405 	public static void cleanupPluginDirectories() throws Exception {
406 		FileUtils.deleteDirectory(getPluginsDirectory());
407 	}
408 
409 	/**
410      * This method searches for an exception of the specified type in the exception stack
411      * @param topLevelException the exception whose stack to traverse
412      * @param exceptionClass the exception class to look for
413      * @return the first instance of an exception of the specified class if found, or null otherwise
414      */
415     public static <T extends Throwable> T findExceptionInStack(Throwable topLevelException, Class<T> exceptionClass) {
416         Throwable t = topLevelException;
417         while (t != null) {
418             if (exceptionClass.isAssignableFrom(t.getClass())) return (T) t;
419             t = t.getCause();
420         }
421         return null;
422     }
423 }