Clean up and add unit tests for AdServer.java Future submissions will similarly improve AdEntity.java and AdAdaptor.java.
diff --git a/src/com/google/enterprise/adaptor/ad/AdServer.java b/src/com/google/enterprise/adaptor/ad/AdServer.java index 33046b0..aaefd1f 100644 --- a/src/com/google/enterprise/adaptor/ad/AdServer.java +++ b/src/com/google/enterprise/adaptor/ad/AdServer.java
@@ -14,7 +14,7 @@ package com.google.enterprise.adaptor.ad; -import com.google.common.base.Strings; +import com.google.common.annotations.VisibleForTesting; import java.io.IOException; import java.sql.Timestamp; @@ -48,18 +48,14 @@ private static final Logger LOGGER = Logger.getLogger(AdServer.class.getName()); - protected LdapContext ldapContext = null; - private SearchControls searchCtls; + private final LdapContext ldapContext; + private final SearchControls searchCtls; // properties necessary for connection - private String hostName; - private int port; - private String principal; - private String password; + private final String hostName; // retrieved properties of the Active Directory controller private String nETBIOSName; - private Method connectMethod; private String dn; private String configurationNamingContext; private String dsServiceName; @@ -67,10 +63,27 @@ private long highestCommittedUSN; private String invocationID; private String dnsRoot; - private Timestamp lastFullSync; public AdServer(Method connectMethod, String hostName, int port, String principal, String password) { + this(hostName, createLdapContext(connectMethod, hostName, port, + principal, password)); + } + + @VisibleForTesting + AdServer(String hostName, LdapContext ldapContext) { + this.hostName = hostName; + this.ldapContext = ldapContext; + searchCtls = new SearchControls(); + searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); + } + + /** + * Called (only) by public constructor + */ + private static LdapContext createLdapContext(Method connectMethod, + String hostName, int port, String principal, String password) { + Hashtable<String, String> env = new Hashtable<String, String>(); if (null == connectMethod || null == hostName || null == principal || null == password) { throw new NullPointerException(); @@ -84,86 +97,63 @@ if ("".equals(password)) { throw new IllegalArgumentException("password needs to be non-empty"); } - this.hostName = hostName; - this.port = port; - this.principal = principal; - this.password = password; - searchCtls = new SearchControls(); - searchCtls.setSearchScope(SearchControls.SUBTREE_SCOPE); - this.connectMethod = connectMethod; + + // Use the built-in LDAP support. + env.put(Context.INITIAL_CONTEXT_FACTORY, + "com.sun.jndi.ldap.LdapCtxFactory"); + // Connecting to configuration naming context is very slow for crawl users + // in large multidomain environment, which belong to thousands of groups + // TODO: make this configurable + env.put("com.sun.jndi.ldap.read.timeout", "90000"); + env.put(Context.SECURITY_AUTHENTICATION, "simple"); + env.put(Context.SECURITY_PRINCIPAL, principal); + env.put(Context.SECURITY_CREDENTIALS, password); + + String ldapUrl = connectMethod.protocol() + hostName + ":" + port; + LOGGER.config("LDAP provider url: " + ldapUrl); + env.put(Context.PROVIDER_URL, ldapUrl); + try { + return new InitialLdapContext(env, null); + } catch (NamingException ne) { + throw new AssertionError(ne); + } } /** * Connects to the Active Directory server and retrieves AD configuration * information. - * - * This method is used for crawling as well as authorization of credentials + * + * This method is used for crawling as well as authorization of credentials * against Active Directory. */ public void connect() throws CommunicationException, NamingException { - Hashtable<String, String> env = new Hashtable<String, String>(); - - // Use the built-in LDAP support. - env.put(Context.INITIAL_CONTEXT_FACTORY, - AdConstants.COM_SUN_JNDI_LDAP_LDAP_CTX_FACTORY); - // Connecting to configuration naming context is very slow for crawl users - // in large multidomain environment, which belong to thousands of groups - // TODO: make this configurable - env.put("com.sun.jndi.ldap.read.timeout", "90000"); - if (Strings.isNullOrEmpty(principal)) { - env.put(Context.SECURITY_AUTHENTICATION, - AdConstants.AUTHN_TYPE_ANONYMOUS); - } else { - env.put(Context.SECURITY_AUTHENTICATION, - AdConstants.AUTHN_TYPE_SIMPLE); - env.put(Context.SECURITY_PRINCIPAL, principal); - env.put(Context.SECURITY_CREDENTIALS, password); - } - - String ldapUrl = - connectMethod.protocol() + hostName + AdConstants.COLON + port; - LOGGER.info("LDAP provider url: " + ldapUrl); - env.put(Context.PROVIDER_URL, ldapUrl); - ldapContext = new InitialLdapContext(env, null); - - Attributes attributes = ldapContext.getAttributes(AdConstants.EMPTY); - dn = attributes.get( - AdConstants.ATTR_DEFAULTNAMINGCONTEXT).get(0).toString(); - dsServiceName = attributes.get( - AdConstants.ATTR_DSSERVICENAME).get(0).toString(); + Attributes attributes = ldapContext.getAttributes(""); + dn = attributes.get("defaultNamingContext").get(0).toString(); + dsServiceName = attributes.get("dsServiceName").get(0).toString(); highestCommittedUSN = Long.parseLong(attributes.get( - AdConstants.ATTR_HIGHESTCOMMITTEDUSN).get(0).toString()); + "highestCommittedUSN").get(0).toString()); configurationNamingContext = attributes.get( - AdConstants.ATTR_CONFIGURATIONNAMINGCONTEXT).get(0).toString(); + "configurationNamingContext").get(0).toString(); } public void initialize() { try { connect(); - sid = AdEntity.getTextSid((byte[])get( - AdConstants.ATTR_DISTINGUISHEDNAME + AdConstants.EQUALS + dn, - AdConstants.ATTR_OBJECTSID, dn)); + sid = AdEntity.getTextSid((byte[]) get( + "distinguishedName=" + dn, "objectSid;binary", dn)); invocationID = AdEntity.getTextGuid((byte[]) get( - AdConstants.ATTR_DISTINGUISHEDNAME - + AdConstants.EQUALS + dsServiceName, - AdConstants.ATTR_INVOCATIONID, - dsServiceName)); - } catch (CommunicationException e) { - throw new IllegalStateException(e); - } catch (AuthenticationNotSupportedException e) { - throw new IllegalStateException(e); - } catch (AuthenticationException e) { - throw new IllegalStateException(e); + "distinguishedName=" + dsServiceName, + "invocationID;binary", dsServiceName)); } catch (NamingException e) { - throw new IllegalStateException(e); + throw new RuntimeException(e); } - LOGGER.info("Sucessfully created an Initial LDAP context"); + LOGGER.info("Successfully created an Initial LDAP context"); nETBIOSName = (String) get("(ncName=" + dn + ")", - AdConstants.ATTR_NETBIOSNAME, configurationNamingContext); - dnsRoot = (String) get("(ncName=" + dn + ")", - AdConstants.ATTR_DNSROOT, configurationNamingContext); + "nETBIOSName", configurationNamingContext); + dnsRoot = (String) get("(ncName=" + dn + ")", "dnsRoot", + configurationNamingContext); LOGGER.log(Level.INFO, "Connected to domain (dn = " + dn + ", netbios = " + nETBIOSName + ", hostname = " + hostName + ", dsServiceName = " + dsServiceName + ", highestCommittedUSN = " + highestCommittedUSN @@ -200,7 +190,7 @@ } /** - * Set request controls on the LDAP query + * Set request controls on the LDAP query * @param deleted include deleted control */ private void setControls(boolean deleted) { @@ -245,7 +235,7 @@ SearchResult sr = ldapResults.next(); try { results.add(new AdEntity(sr)); - } catch (Exception ex) { + } catch (Exception ex) { // It is possible that Search Result returned is missing // few attributes required to construct AD Entity object. // Such results will be ignored. @@ -279,8 +269,8 @@ int batch = g.getMembers().size(); int start = g.getMembers().size(); do { - String memberRange = String.format(AdConstants.ATTR_MEMBER_RANGE, - start, start + batch - 1); + String memberRange = String.format("member;Range=%d-%d", start, + start + batch - 1); LOGGER.finest( "Retrieving additional groups for [" + g + "] " + memberRange); searchCtls.setReturningAttributes(new String[] {memberRange}); @@ -305,26 +295,6 @@ } /** - * Generate properties to be used for parameter binding in JDBC - * @return map of names and properties of current object - */ - public Map<String, Object> getSqlParams() { - HashMap<String, Object> map = new HashMap<String, Object>(); - map.put(AdConstants.DB_DN, dn); - map.put(AdConstants.DB_DSSERVICENAME, dsServiceName); - map.put(AdConstants.DB_INVOCATIONID, invocationID); - map.put(AdConstants.DB_HIGHESTCOMMITTEDUSN, highestCommittedUSN); - map.put(AdConstants.DB_NETBIOSNAME, nETBIOSName); - map.put(AdConstants.DB_SID, sid); - map.put(AdConstants.DB_DNSROOT, dnsRoot); - if (lastFullSync != null) { - map.put(AdConstants.DB_LASTFULLSYNC, - new java.sql.Timestamp(lastFullSync.getTime())); - } - return map; - } - - /** * @return the distinguished Name */ public final String getDn() { @@ -374,20 +344,6 @@ } } - /** - * @return the lastFullSync - */ - public Timestamp getLastFullSync() { - return lastFullSync; - } - - /** - * @param lastFullSync the lastFullSync to set - */ - public void setLastFullSync(Timestamp lastFullSync) { - this.lastFullSync = lastFullSync; - } - @Override public String toString() { return "[" + nETBIOSName + "] ";
diff --git a/test/com/google/enterprise/adaptor/ad/AdServerTest.java b/test/com/google/enterprise/adaptor/ad/AdServerTest.java new file mode 100644 index 0000000..b80a81a --- /dev/null +++ b/test/com/google/enterprise/adaptor/ad/AdServerTest.java
@@ -0,0 +1,415 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache 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.apache.org/licenses/LICENSE-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 com.google.enterprise.adaptor.ad; + +import static org.junit.Assert.*; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import javax.naming.*; +import javax.naming.directory.*; +import javax.naming.ldap.*; + +import java.util.*; + +/** Test cases for {@link AdServer}. */ +public class AdServerTest { + @Rule + public ExpectedException thrown = ExpectedException.none(); + + @Test + public void testStandardServer() throws Exception { + AdServer adServer = new AdServer("hostname", new MockLdapContext()); + assertEquals("hostname", adServer.getHostName()); + } + + @Test + public void testNPEOnNullConnectMethod() { + thrown.expect(NullPointerException.class); + AdServer adServer = new AdServer(null, "hostname", 1234, "principal", "pw"); + } + + @Test + public void testNPEOnNullHostname() { + thrown.expect(NullPointerException.class); + AdServer adServer = new AdServer(Method.SSL, null, 1234, "principal", "pw"); + } + + @Test + public void testIAEOnEmptyHostname() { + thrown.expect(IllegalArgumentException.class); + AdServer adServer = new AdServer(Method.SSL, "", 1234, "principal", "pw"); + } + + @Test + public void testNPEOnNullPrincipal() { + thrown.expect(NullPointerException.class); + AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, null, "pw"); + } + + @Test + public void testIAEOnEmptyPrincipal() throws Exception { + thrown.expect(IllegalArgumentException.class); + AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, "", "pw"); + } + + @Test + public void testNPEOnNullPassword() { + thrown.expect(NullPointerException.class); + AdServer adServer = new AdServer(Method.SSL, "host", 1234, "princ", null); + } + + @Test + public void testIAEOnEmptyPassword() { + thrown.expect(IllegalArgumentException.class); + AdServer adServer = new AdServer(Method.SSL, "hostname", 1234, "princ", ""); + } + + @Test + public void testPublicConstructor() { + thrown.expect(AssertionError.class); + AdServer adServer = new AdServer(Method.SSL, "localhost", 389, " ", " "); + } + + @Test + public void testStandardServerInitialize() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + ldapContext.addSearchResult("dn=empty", "empty", "empty", "") + .addSearchResult("dn=empty", "attr1", "basedn", "val1"); + AdServer adServer = new AdServer("localhost", ldapContext); + assertEquals("localhost", adServer.getHostName()); + adServer.initialize(); + assertEquals("DN_for_default_naming_context", adServer.getDn()); + assertEquals("ds_service_name", adServer.getDsServiceName()); + assertEquals(12345678L, adServer.getHighestCommittedUSN()); + assertEquals("S-1-0-0", adServer.getSid()); + assertEquals("0x0123456789abc", adServer.getInvocationID()); + assertEquals("GSA-CONNECTORS", adServer.getnETBIOSName()); + assertEquals("[GSA-CONNECTORS] ", adServer.toString()); + assertEquals("", adServer.get("dn=empty", "empty", "empty")); + assertNull(adServer.get("dn=ds_service_name", "null", "null")); + assertEquals("val1", adServer.get("dn=empty", "attr1", "basedn")); + assertNull(adServer.get("dn=empty", "attr2", "basedn")); + } + + @Test + /* + * This tests a code path that the author doesn't think can actually happen + * + * <p> where <code>ldapResults</code> is not <code>null</code>, but where + * <code>attrs.get(attribute)</code> does return <code>null</code>. + */ + public void testGetNotReturningAttribute() throws Exception { + MockLdapContext ldapContext = new MockLdapContext() { + @Override + public NamingEnumeration<SearchResult> search(String base, String filter, + SearchControls searchControls) throws NamingException { + if (!("dn=empty".equals(filter))) { + return super.search(base, filter, searchControls); + } + // prepare "broken" SearchResult + Vector<SearchResult> brokenSRs = new Vector<SearchResult>(); + brokenSRs.add(new SearchResult("search result name", brokenSRs, + new BasicAttributes())); + return new MockLdapContext.SearchResultsNamingEnumeration(brokenSRs); + } + }; + addStandardKeysAndResults(ldapContext); + ldapContext.addSearchResult("dn=empty", "attr1", "basedn", "val1"); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + assertNull(adServer.get("dn=empty", "attr1", "basedn")); + } + + @Test + public void testGetThrowsNamingException() throws Exception { + MockLdapContext ldapContext = new MockLdapContext() { + @Override + public NamingEnumeration<SearchResult> search(String base, String filter, + SearchControls searchControls) throws NamingException { + if (!("dn=empty".equals(filter))) { + return super.search(base, filter, searchControls); + } + throw new NamingException("Gotcha"); + } + }; + addStandardKeysAndResults(ldapContext); + ldapContext.addSearchResult("dn=empty", "attr1", "basedn", "val1"); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + assertNull(adServer.get("dn=empty", "attr1", "basedn")); + } + + @Test + public void testConnectThrowsNamingException() throws Exception { + thrown.expect(RuntimeException.class); + MockLdapContext ldapContext = new MockLdapContext() { + @Override + public Attributes getAttributes(String name) throws NamingException { + throw new NamingException("Can't connect"); + } + }; + addStandardKeysAndResults(ldapContext); + ldapContext.addSearchResult("dn=empty", "attr1", "basedn", "val1"); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + } + + @Test + public void testSearchReturnsOneUser() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String userDn = "DN_for_default_naming_context"; + ldapContext.addSearchResult(filter, "cn", userDn, "user1") + .addSearchResult(filter, "primaryGroupId", userDn, "users") + .addSearchResult(filter, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, false, + new String[] { "cn", "primaryGroupId", "objectGUID;binary" }); + assertEquals(1, resultSet.size()); + for (AdEntity ae : resultSet) { + assertEquals("name under", ae.getCommonName()); + } + } + + @Test + public void testSearchReturnsNoUsersWhenMissingGUID() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String userDn = "DN_for_default_naming_context"; + ldapContext.addSearchResult(filter, "cn", userDn, "user1") + .addSearchResult(filter, "primaryGroupId", userDn, "users"); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, false, + new String[] { "cn", "primaryGroupId", "objectGUID;binary" }); + assertEquals(0, resultSet.size()); + } + + @Test + public void testSetControlsThrowsException() throws Exception { + MockLdapContext ldapContext = new MockLdapContext() { + @Override + public void setRequestControls(Control[] requestControls) + throws NamingException { + controls = requestControls; + throw new NamingException("testing exception path"); + } + }; + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String userDn = "DN_for_default_naming_context"; + ldapContext.addSearchResult(filter, "cn", userDn, "user1") + .addSearchResult(filter, "primaryGroupId", userDn, "users"); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, false, + new String[] { "cn", "primaryGroupId", "objectGUID;binary" }); + assertEquals(0, resultSet.size()); + } + + @Test + public void testSearchReturnsOneDeletedUser() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String userDn = "DN_for_default_naming_context"; + ldapContext.addSearchResult(filter, "cn", userDn, "user1") + .addSearchResult(filter, "primaryGroupId", userDn, "users") + .addSearchResult(filter, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, true, + new String[] { "cn", "primaryGroupId", "objectGUID;binary" }); + assertEquals(1, resultSet.size()); + for (AdEntity ae : resultSet) { + assertEquals("name under", ae.getCommonName()); + } + // now verify the DeletedControl is exactly as we expect + assertEquals(2, ldapContext.getResponseControls().length); + Control deletedControl = ldapContext.getResponseControls()[1]; + assertArrayEquals(new byte[0], deletedControl.getEncodedValue()); + assertEquals("1.2.840.113556.1.4.417", deletedControl.getID()); + assertTrue(deletedControl.isCritical()); + } + + @Test + public void testSearchReturnsOneEmptyGroup() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String userDn = "DN_for_default_naming_context"; + ldapContext.addSearchResult(filter, "cn", userDn, "users") + .addSearchResult(filter, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, true, + new String[] { "cn", "members", "objectGUID;binary" }); + assertEquals(1, resultSet.size()); + for (AdEntity ae : resultSet) { + assertEquals("name under", ae.getCommonName()); + } + } + + @Test + public void testSearchReturnsOneNonemptyGroup() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String userDn = "DN_for_default_naming_context"; + List<String> members = Arrays.asList("dn_for_user_1", "dn_for_user_2"); + ldapContext.addSearchResult(filter, "cn", userDn, "users") + .addSearchResult(filter, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")) + .addSearchResult(filter, "member", userDn, members); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, false, + new String[] { "cn", "member", "objectGUID;binary" }); + assertEquals(1, resultSet.size()); + for (AdEntity ae : resultSet) { + assertEquals(new HashSet<String>(members), ae.getMembers()); + } + } + + @Test + public void testSearchReturnsMembersinTwoRanges() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String filter2 = "(sAMAccountName=sam)"; + final String userDn = "DN_for_default_naming_context"; + List<String> members = Arrays.asList("dn_for_user_0", "dn_for_user_1"); + List<String> moreMembers = Arrays.asList("dn_for_user_2", "dn_for_user_3"); + ldapContext.addSearchResult(filter, "cn", userDn, "users") + .addSearchResult(filter, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")) + .addSearchResult(filter, "sAMAccountName", userDn, "sam") + .addSearchResult(filter, "member;Range=0-1", userDn, members); + ldapContext.addSearchResult(filter2, "cn", userDn, "users") + .addSearchResult(filter2, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")) + .addSearchResult(filter2, "sAMAccountName", userDn, "sam2") + .addSearchResult(filter2, "member;Range=2-3*", userDn, + moreMembers); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, false, + // need the ranged members for MockLdapContext, not for "real" AD. + new String[] { "cn", "member", "member;Range=0-1", "member;Range=2-3", + "objectGUID;binary", "sAMAccountName" }); + assertEquals(1, resultSet.size()); + HashSet<String> expectedMembers = new HashSet<String>(members); + expectedMembers.addAll(moreMembers); + for (AdEntity ae : resultSet) { + assertEquals(expectedMembers, ae.getMembers()); + } + } + + @Test + public void testSearchReturnsMembersinThreeRanges() throws Exception { + MockLdapContext ldapContext = new MockLdapContext(); + addStandardKeysAndResults(ldapContext); + // populate additional attributes with values we can test + final String filter = "ou=Users"; + final String filter2 = "(sAMAccountName=sam)"; + final String userDn = "DN_for_default_naming_context"; + List<String> members = Arrays.asList("dn_for_user_0", "dn_for_user_1"); + List<String> moreMembers = Arrays.asList("dn_for_user_2", "dn_for_user_3"); + List<String> evenMore = Arrays.asList("dn_for_user_4"); + ldapContext.addSearchResult(filter, "cn", userDn, "users") + .addSearchResult(filter, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")) + .addSearchResult(filter, "sAMAccountName", userDn, "sam") + .addSearchResult(filter, "member;Range=0-1", userDn, members); + ldapContext.addSearchResult(filter2, "cn", userDn, "users") + .addSearchResult(filter2, "objectGUID;binary", userDn, + hexStringToByteArray("000102030405060708090a0b0c")) + .addSearchResult(filter2, "sAMAccountName", userDn, "sam2") + .addSearchResult(filter2, "member;Range=2-3", userDn, + moreMembers) + .addSearchResult(filter2, "member;Range=4-5*", userDn, + evenMore); + AdServer adServer = new AdServer("localhost", ldapContext); + adServer.initialize(); + Set<AdEntity> resultSet = adServer.search(filter, false, + // need the ranged members for MockLdapContext, not for "real" AD. + new String[] { "cn", "member", "member;Range=0-1", "member;Range=2-3", + "member;Range=4-5*", "objectGUID;binary", + "sAMAccountName" }); + assertEquals(1, resultSet.size()); + HashSet<String> expectedMembers = new HashSet<String>(members); + expectedMembers.addAll(moreMembers); + expectedMembers.addAll(evenMore); + for (AdEntity ae : resultSet) { + assertEquals(expectedMembers, ae.getMembers()); + } + } + + /** + * Generate a common LdapContext used for various tests above + */ + private void addStandardKeysAndResults(MockLdapContext ldapContext) { + // populate the attributes with values we can test + ldapContext.addKey("defaultNamingContext", "DN_for_default_naming_context") + .addKey("dsServiceName", "ds_service_name") + .addKey("highestCommittedUSN", "12345678") + .addKey("configurationNamingContext", "naming_context") + .addSearchResult( + "distinguishedName=DN_for_default_naming_context", + "objectSid;binary", + "DN_for_default_naming_context", + hexStringToByteArray("010100000000000000000000")) // S-1-0-0 + .addSearchResult("distinguishedName=ds_service_name", + "invocationID;binary", + "ds_service_name", + hexStringToByteArray("000102030405060708090a0b0c")) + .addSearchResult("(ncName=DN_for_default_naming_context)", + "nETBIOSName", + "naming_context", + "GSA-CONNECTORS") + .addSearchResult("(ncName=DN_for_default_naming_context)", + "dnsRoot", + "naming_context", + "gsa-connectors.com"); + } + + public static byte[] hexStringToByteArray(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + + Character.digit(s.charAt(i+1), 16)); + } + return data; + } +}
diff --git a/test/com/google/enterprise/adaptor/ad/MockLdapContext.java b/test/com/google/enterprise/adaptor/ad/MockLdapContext.java new file mode 100644 index 0000000..cfc87ac --- /dev/null +++ b/test/com/google/enterprise/adaptor/ad/MockLdapContext.java
@@ -0,0 +1,175 @@ +// Copyright 2013 Google Inc. All Rights Reserved. +// +// Licensed under the Apache 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.apache.org/licenses/LICENSE-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 com.google.enterprise.adaptor.ad; + +import com.google.common.annotations.VisibleForTesting; + +import java.io.*; +import java.util.*; + +import javax.naming.*; +import javax.naming.directory.*; +import javax.naming.ldap.Control; +import javax.naming.ldap.InitialLdapContext; + +/** + * Mock of {@link LdapContext}. + */ +public class MockLdapContext extends InitialLdapContext { + private BasicAttributes attributes = new BasicAttributes(); + private Hashtable<String, Object> searchResults + = new Hashtable<String, Object>(); + @VisibleForTesting + Control[] controls = new Control[0]; + + public MockLdapContext() throws NamingException { + } + + @Override + public Attributes getAttributes(String name) throws NamingException { + return attributes; + } + + /** Normal way to add things to this Mock */ + public MockLdapContext addKey(String key, Object value) { + attributes.put(key, value); + return this; + } + + public MockLdapContext removeKey(String key) { + attributes.remove(key); + return this; + } + + public static String makeKey(String filter, String attribute, String baseDn) { + return filter + "|" + attribute + "|" + baseDn; + } + + public MockLdapContext addSearchResult(String filter, + String attribute, String baseDn, Object value) { + searchResults.put(makeKey(filter, attribute, baseDn), value); + return this; + } + + /** Returns a <code>NamingEnumeration</code> of <code>SearchResult</code>s */ + public NamingEnumeration<SearchResult> search(String base, String filter, + SearchControls searchControls) throws NamingException { + Vector<SearchResult> results = new Vector<SearchResult>(); + SearchResult currentResult = null; + for (String attribute : searchControls.getReturningAttributes()) { + Object o = searchResults.get(makeKey(filter, attribute, base)); + if (o == null) { + // hack to make search for member;Range=X-Y succeed when that's the + // final range present + if (attribute.startsWith("member;Range=")) { + attribute += "*"; + o = searchResults.get(makeKey(filter, attribute, base)); + } + } + if (o != null) { + if (currentResult == null) { + currentResult = makeSearchResult(attribute, o, base); + results.add(currentResult); + } else { + Attributes attrs = currentResult.getAttributes(); + this.addAttribute(attrs, attribute, o); + currentResult.setAttributes(attrs); + } + } + } + return new SearchResultsNamingEnumeration(results); + } + + /** properly handle collections as we add a new attribute */ + private static void addAttribute(Attributes attrs, String newAttr, + Object value) { + attrs.put(newAttr, value); + if (value instanceof Collection<?>) { + Attribute attr = attrs.get(newAttr); + attr.clear(); + for (Object member: (Collection<?>) value) { + attr.add(member); + } + } + } + + /** Creates a <code>SearchResults</code> wrapper with a single attribute */ + private static SearchResult makeSearchResult(String attribute, Object o, + String baseDn) { + SearchResult sr = new SearchResult("search result name", o, + new BasicAttributes()); + addAttribute(sr.getAttributes(), attribute, o); + sr.setNameInNamespace("cn=name\\ under," + baseDn); + return sr; + } + + @Override + public Control[] getResponseControls() throws NamingException { + return controls; + }; + + /** Sets the controls (which this class does nothing with) */ + @Override + public void setRequestControls(Control[] requestControls) + throws NamingException { + controls = requestControls; + }; + + @VisibleForTesting + static class SearchResultsNamingEnumeration + implements NamingEnumeration<SearchResult> { + private final Enumeration<SearchResult> results; + private SearchResult nextElement = null; + + SearchResultsNamingEnumeration(Vector<SearchResult> results) { + this.results = results.elements(); + } + + private SearchResult getNextElement() { + if (results.hasMoreElements()) { + return results.nextElement(); + } + return null; + } + + public boolean hasMore() { + if (nextElement != null) { + return true; + } + nextElement = getNextElement(); + return (nextElement != null); + } + + public boolean hasMoreElements() { + return hasMore(); + } + + public SearchResult next() { + if (!hasMore()) { + throw new NoSuchElementException(); + } + SearchResult res = nextElement; + nextElement = null; + return res; + } + + public SearchResult nextElement() { + return next(); + } + + public void close() { + } + } +}