/********************************************************************************** * * $Id: GetUsersByEidTest.java 122028 2013-04-01 19:49:35Z azeckoski@unicon.net $ * *********************************************************************************** * * Copyright (c) 2008 The Sakai Foundation * * Licensed under the Educational Community License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.opensource.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * **********************************************************************************/ package org.sakaiproject.user.impl.test; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import junit.extensions.TestSetup; import junit.framework.Assert; import junit.framework.Test; import junit.framework.TestSuite; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.sakaiproject.authz.api.AuthzGroupService; import org.sakaiproject.memory.api.Cache; import org.sakaiproject.memory.api.MemoryService; import org.sakaiproject.test.SakaiKernelTestBase; import org.sakaiproject.thread_local.api.ThreadLocalManager; import org.sakaiproject.tool.api.SessionManager; import org.sakaiproject.user.api.User; import org.sakaiproject.user.api.UserDirectoryProvider; import org.sakaiproject.user.api.UserEdit; import org.sakaiproject.user.impl.DbUserService; /** * This is a white-box-ish test which uses inner knowledge of the current * UserDirectoryService implementation. */ public class GetUsersByEidTest extends SakaiKernelTestBase { private static Log log = LogFactory.getLog(GetUsersByEidTest.class); // Oracle will throw a SQLException if we put more than this into a // "WHERE tbl.col IN (:paramList)" query, and so we need to test for // that condition. private static int MAX_NUMBER_OF_SQL_PARAMETERS_IN_LIST = 1000; private static String USER_EMAIL_PREFIX = "_user@some.edu"; private static String USER_FIRST_NAME_PREFIX = "F. "; private static String USER_LAST_NAME_PREFIX = "de "; private static String LOCAL_USER_EID = "local_maint"; private static String LOCAL_USER_WITH_NO_METADATA_EID = "very_private"; private static String NO_SUCH_EID = "no_way_no_how"; private static String SURPRISE_FOR_EID_TEST_EID = "provided_for_eid_test"; private static List<String> mappedUserIds = new ArrayList<String>(); // This is static because we can't completely undo UserDirectoryService actions // in a normal tearDown, and so we need to load data for all tests in a // one-time setup. // This is the implementation class because there's no way to inject the // test provider or to clear the user cache through the official API. private static DbUserService dbUserService; private static Cache callCache; private static AuthzGroupService authzGroupService; private static ThreadLocalManager threadLocalManager; private static SessionManager sessionManager; public static Test suite() { TestSetup setup = new TestSetup(new TestSuite(GetUsersByEidTest.class)) { protected void setUp() throws Exception { try { oneTimeSetup("disable_user_cache"); oneTimeSetupAfter(); } catch (Exception e) { log.warn(e); } } protected void tearDown() throws Exception { oneTimeTearDown(); } }; return setup; } public static void oneTimeSetupAfter() throws Exception { TestProvider userDirectoryProvider = new TestProvider(); // This is a workaround until we can make it easier to load sakai.properties // for specific integration tests. dbUserService = (DbUserService)getService("org.sakaiproject.user.api.UserDirectoryService"); dbUserService.setProvider(userDirectoryProvider); callCache = ((MemoryService) getService("org.sakaiproject.memory.api.MemoryService")).newCache( "org.sakaiproject.user.api.UserDirectoryService.callCache", "/user/"); authzGroupService = (AuthzGroupService) getService(AuthzGroupService.class); threadLocalManager = (ThreadLocalManager) getService(ThreadLocalManager.class); sessionManager = (SessionManager) getService(SessionManager.class); // Sakai provides no way to undo a EID-to-ID mapping, and so we can't use // a normal setUp and tearDown approach to loading test data. // Act as admin since we're checking functionality rather than authz. actAsAdmin(); // Add two local users for honesty's sake. User user; user = dbUserService.addUser(null, LOCAL_USER_EID, "Joe", "Guest", "joe@somewhere.edu", "pw", "Student", null); mappedUserIds.add(user.getId()); // Store for later use clearUserFromServiceCaches(user.getId()); //for the search later we want a similar user user = dbUserService.addUser(null, "thisIs a search user", "Joe", "Guest", "joe@somewhere.edu", "pw", "Student", null); // The User Directory Service implementation currently includes no metadata that // distinguishes a metadata-free Sakai-managed user from a provided user, and so // no field of a full join can be safely checked to decide whether the provider // needs to be called. To make sure no erroneous assumptions get made, create a // mostly-null user record. user = dbUserService.addUser(null, LOCAL_USER_WITH_NO_METADATA_EID); mappedUserIds.add(user.getId()); // Store for later use clearUserFromServiceCaches(user.getId()); // Our interest is in testing retrieval rather than creation. Pre-load all the // EID-to-ID mappings before doing the searches. for (int providedCounter = 0; providedCounter < MAX_NUMBER_OF_SQL_PARAMETERS_IN_LIST; providedCounter++) { user = dbUserService.getUserByEid(String.valueOf(providedCounter)); mappedUserIds.add(user.getId()); // Store for later use clearUserFromServiceCaches(user.getId()); } } public void testGetUsersByEid() throws Exception { // Our big search list should contain: // - All the legitimate provided user EIDs. // - The two Sakai-managed users, including one with null metadata. // - Continued case-insensitive EIDs by default // - A bogus EID which won't match anyone. // - An EID for a newly provided user who hasn't been preloaded. List<String> searchEids = new ArrayList<String>(); searchEids.add(LOCAL_USER_EID.toUpperCase()); searchEids.add(LOCAL_USER_WITH_NO_METADATA_EID); searchEids.add(NO_SUCH_EID); for (int providedCounter = 0; providedCounter < MAX_NUMBER_OF_SQL_PARAMETERS_IN_LIST; providedCounter++) { searchEids.add(String.valueOf(providedCounter)); } searchEids.add(SURPRISE_FOR_EID_TEST_EID); // Previously unseen // What we're really interested in is the number of DB queries, but // we don't yet have an easy way to monitor that. Instead, we make // sure that the provider methods are being called in the most efficient // way possible. TestProvider.GET_USER_CALLS_COUNTER = 0; TestProvider.GET_USERS_CALLS_COUNTER = 0; List<User> users = dbUserService.getUsersByEids(searchEids); Assert.assertEquals(MAX_NUMBER_OF_SQL_PARAMETERS_IN_LIST + 3, users.size()); // Everyone but the NO_SUCH_EID searchEids.remove(NO_SUCH_EID); Assert.assertEquals(0, TestProvider.GET_USER_CALLS_COUNTER); Assert.assertEquals(1, TestProvider.GET_USERS_CALLS_COUNTER); // Make sure caching wasn't broken. Again we need to use our inside // knowledge that even when all other caching is turned off, the // UDS ThreadLocal cache should keep the provider from needing to // be called. TestProvider.GET_USER_CALLS_COUNTER = 0; for (String eid : searchEids) { User user = dbUserService.getUserByEid(eid); clearUserFromServiceCaches(user.getId()); } Assert.assertEquals(0, TestProvider.GET_USER_CALLS_COUNTER); } public void testGetUsersById() throws Exception { // Our big search list should contain: // - All the existing IDs for legitimate provided users. // - The two Sakai-managed users, including one with null metadata. // - A bogus ID which won't match anyone. List<String> searchIds = new ArrayList<String>(mappedUserIds); searchIds.add(NO_SUCH_EID); // What we're really interested in is the number of DB queries, but // we don't yet have an easy way to monitor that. Instead, we make // sure that the provider methods are being called in the most efficient // way possible. TestProvider.GET_USER_CALLS_COUNTER = 0; TestProvider.GET_USERS_CALLS_COUNTER = 0; List<User> users = dbUserService.getUsers(searchIds); Assert.assertEquals(mappedUserIds.size(), users.size()); // Everyone but the NO_SUCH_EID Assert.assertEquals(0, TestProvider.GET_USER_CALLS_COUNTER); Assert.assertEquals(1, TestProvider.GET_USERS_CALLS_COUNTER); // Make sure caching wasn't broken. Again we need to use our inside // knowledge that even when all other caching is turned off, the // UDS ThreadLocal cache should keep the provider from needing to // be called. TestProvider.GET_USER_CALLS_COUNTER = 0; for (String id : mappedUserIds) { User user = dbUserService.getUser(id); clearUserFromServiceCaches(user.getId()); } Assert.assertEquals(0, TestProvider.GET_USER_CALLS_COUNTER); } public void testSearchUsers() { List<User> users = dbUserService.searchUsers("Joe", 1, 1); if (users == null) { log.error("empty list from search"); fail(); } else if (users.size() == 0 || users.size() > 1) { log.error("list of size 1 expected list contained: " + users.size()); fail(); } } private static void actAsAdmin() { sessionManager.getCurrentSession().setUserId("admin"); authzGroupService.refreshUser("admin"); } private static void clearUserFromServiceCaches(String userId) { dbUserService.getIdEidCache().removeAll(); String ref = "/user/" + userId; threadLocalManager.set(ref, null); if (callCache != null) { callCache.remove(ref); } } public static class TestProvider implements UserDirectoryProvider { public static int GET_USER_CALLS_COUNTER = 0; public static int GET_USERS_CALLS_COUNTER = 0; public boolean authenticateUser(String eid, UserEdit userEdit, String password) { return false; } public boolean authenticateWithProviderFirst(String eid) { return false; } public boolean findUserByEmail(UserEdit userEdit, String email) { return false; } private boolean fillUserRecord(UserEdit userEdit) { String userEid = userEdit.getEid(); // If the EID isn't numeric, we have a ringer. if (!userEid.matches("^\\d+$") && !SURPRISE_FOR_EID_TEST_EID.equals(userEid)) { return false; } userEdit.setEmail(userEid + USER_EMAIL_PREFIX); userEdit.setFirstName(USER_FIRST_NAME_PREFIX + userEid); userEdit.setLastName(USER_LAST_NAME_PREFIX + userEid); return true; } public boolean getUser(UserEdit userEdit) { GET_USER_CALLS_COUNTER++; return fillUserRecord(userEdit); } @SuppressWarnings("unchecked") public void getUsers(Collection users) { GET_USERS_CALLS_COUNTER++; // This is where an efficient single DB query might // be made if we used a DB.... // We're forced to use an iterator here. We can't explicitly // remove an unmatched user record from the input collection // because BaseUserEdit.equals() is based purely on getID(), // which will be null for all unmapped users. for (Iterator<UserEdit> userIter = users.iterator(); userIter.hasNext(); ) { UserEdit userEdit = userIter.next(); if (!fillUserRecord(userEdit)) { userIter.remove(); } } } } }