Merge branch 'master' of https://code.google.com/p/plexi.ad
diff --git a/src/com/google/enterprise/adaptor/ad/AdServer.java b/src/com/google/enterprise/adaptor/ad/AdServer.java
index 33046b0..b125701 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 LdapContext ldapContext = null;
private 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,64 @@
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.info("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));
+ "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 +191,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 +236,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 +270,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 +296,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 +345,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..da5d5eb
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/ad/AdServerTest.java
@@ -0,0 +1,393 @@
+// 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);
+ MockLdapContext ldapContext = new MockLdapContext();
+ 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 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..1cfcf68
--- /dev/null
+++ b/test/com/google/enterprise/adaptor/ad/MockLdapContext.java
@@ -0,0 +1,183 @@
+// 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 Config}.
+ */
+public class MockLdapContext extends InitialLdapContext {
+ private BasicAttributes attributes = new BasicAttributes();
+ private Hashtable<String, Object> searchResults
+ = new Hashtable<String, Object>();
+ private Control[] controls = new Control[0];
+
+ // A hack to throw exactly 2 exceptions on calls to setRequestControls()
+ static int numberOfTimesSetRequestControlsWasCalled = 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)
+ *
+ * <p>throws Exceptions exactly once, to test the Exception processing code.
+ */
+ @Override
+ public void setRequestControls(Control[] requestControls)
+ throws NamingException {
+ controls = requestControls;
+ if (++numberOfTimesSetRequestControlsWasCalled == 2) {
+ throw new NamingException("calling setRequestControls for second time");
+ }
+ };
+
+ @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() {
+ }
+ }
+}