From 977a120eb36c5ddaa41ddabadd1f546bce10cfce Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Fri, 09 Sep 2011 10:16:46 +0000
Subject: [PATCH] Issue OPENDJ-262: Implement pass through authentication (PTA)

---
 opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java |  692 ++++++++++++++++++++++++++++++++++++++++++++++++++-------
 opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java                          |   18 
 opends/src/messages/messages/extension.properties                                                                       |    7 
 3 files changed, 614 insertions(+), 103 deletions(-)

diff --git a/opends/src/messages/messages/extension.properties b/opends/src/messages/messages/extension.properties
index bf80d19..9ca6451 100644
--- a/opends/src/messages/messages/extension.properties
+++ b/opends/src/messages/messages/extension.properties
@@ -1489,12 +1489,9 @@
 MILD_ERR_LDAP_PTA_CONNECTION_SEARCH_SIZE_LIMIT_603=The remote LDAP server at %s:%d \
  for LDAP PTA policy "%s" returned multiple matching entries while searching \
  "%s" using the filter "%s"
-MILD_ERR_LDAP_PTA_CONNECTION_SEARCH_TIME_LIMIT_604=The remote LDAP server at %s:%d \
- for LDAP PTA policy "%s" failed to return any matching entries within the timeout \
- while  searching "%s" using the filter "%s"
-MILD_ERR_LDAP_PTA_CONNECTION_SEARCH_NO_MATCHES_605=The remote LDAP server at %s:%d \
+MILD_ERR_LDAP_PTA_CONNECTION_SEARCH_NO_MATCHES_604=The remote LDAP server at %s:%d \
  for LDAP PTA policy "%s" did not return any matching entries while searching \
  "%s" using the filter "%s"
-MILD_ERR_LDAP_PTA_CONNECTION_SEARCH_FAILED_606=The remote LDAP server at %s:%d \
+MILD_ERR_LDAP_PTA_CONNECTION_SEARCH_FAILED_605=The remote LDAP server at %s:%d \
  for LDAP PTA policy "%s" returned an error while searching "%s" using the \
  filter "%s": response code %d (%s) and error message "%s"
diff --git a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 3412387..2f23376 100644
--- a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -290,7 +290,7 @@
           new SearchRequestProtocolOp(
             ByteString.valueOf(baseDN.toString()), scope,
             DereferencePolicy.DEREF_ALWAYS, 1 /* size limit */,
-            (timeoutMS / 1000), false /* types only */,
+            (timeoutMS / 1000), true /* types only */,
             RawFilter.create(filter), NO_ATTRIBUTES);
         sendRequest(searchRequest);
 
@@ -310,7 +310,7 @@
           case OP_TYPE_SEARCH_RESULT_ENTRY:
             final SearchResultEntryProtocolOp searchEntry = responseMessage
                 .getSearchResultEntryProtocolOp();
-            if (username != null)
+            if (username == null)
             {
               username = ByteString.valueOf(searchEntry.getDN().toString());
             }
@@ -343,13 +343,6 @@
                       String.valueOf(options.dn()), String.valueOf(baseDN),
                       String.valueOf(filter)));
 
-            case TIME_LIMIT_EXCEEDED:
-              // The server timed out the search.
-              throw new DirectoryException(ResultCode.CLIENT_SIDE_TIMEOUT,
-                  ERR_LDAP_PTA_CONNECTION_SEARCH_TIME_LIMIT.get(host, port,
-                      String.valueOf(options.dn()), String.valueOf(baseDN),
-                      String.valueOf(filter)));
-
             default:
               // The search failed for some reason.
               throw new DirectoryException(resultCode,
@@ -1712,8 +1705,10 @@
   // Debug tracer for this class.
   private static final DebugTracer TRACER = DebugLogger.getTracer();
 
-  // Attribute list for searches requesting no attributes.
-  private static final LinkedHashSet<String> NO_ATTRIBUTES;
+  /**
+   * Attribute list for searches requesting no attributes.
+   */
+  static final LinkedHashSet<String> NO_ATTRIBUTES;
 
   static
   {
@@ -1761,6 +1756,7 @@
     case OTHER:
     case UNWILLING_TO_PERFORM:
     case OPERATIONS_ERROR:
+    case TIME_LIMIT_EXCEEDED:
     case CLIENT_SIDE_CONNECT_ERROR:
     case CLIENT_SIDE_DECODING_ERROR:
     case CLIENT_SIDE_ENCODING_ERROR:
diff --git a/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java b/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
index 28e3bad..587f244 100644
--- a/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
+++ b/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -29,6 +29,7 @@
 
 
 import static org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.isFatalResultCode;
+import static org.opends.server.protocols.ldap.LDAPConstants.OID_NOTICE_OF_DISCONNECTION;
 import static org.testng.Assert.*;
 
 import java.io.IOException;
@@ -51,9 +52,7 @@
 import org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.LDAPConnectionFactory;
 import org.opends.server.protocols.asn1.ASN1;
 import org.opends.server.protocols.asn1.ASN1Writer;
-import org.opends.server.protocols.ldap.LDAPMessage;
-import org.opends.server.protocols.ldap.ProtocolOp;
-import org.opends.server.protocols.ldap.UnbindRequestProtocolOp;
+import org.opends.server.protocols.ldap.*;
 import org.opends.server.tools.LDAPReader;
 import org.opends.server.tools.LDAPWriter;
 import org.opends.server.types.*;
@@ -424,7 +423,7 @@
     private MappingPolicy mappingPolicy = MappingPolicy.UNMAPPED;
     private final SortedSet<String> primaryServers = new TreeSet<String>();
     private final SortedSet<String> secondaryServers = new TreeSet<String>();
-    private int timeoutMS = 3000;
+    private int timeoutMS = 0; // unlimited
 
 
 
@@ -1320,7 +1319,8 @@
     {
       // No valid connections available so this should always fail with
       // INVALID_CREDENTIALS.
-      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+          e.getMessage());
     }
 
     // There should be no more pending events.
@@ -1394,7 +1394,8 @@
     {
       // No valid connections available so this should always fail with
       // INVALID_CREDENTIALS.
-      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+          e.getMessage());
     }
 
     // Tear down and check final state.
@@ -1490,7 +1491,8 @@
     {
       // No valid connections available so this should always fail with
       // INVALID_CREDENTIALS.
-      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+          e.getMessage());
     }
 
     // Tear down and check final state.
@@ -1521,85 +1523,6 @@
 
 
   /**
-   * Checks connect and immediately unbind work ok.
-   *
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = true)
-  public void testLDAPConnectionFactoryConnectAndUnbind() throws Exception
-  {
-    // Mock configuration.
-    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
-
-    // Mock server.
-    final MockServer server = mockServer(cfg).thenAccept()
-        .thenReceive(1, new UnbindRequestProtocolOp()).thenClose().start();
-
-    // Test connect and close.
-    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
-        "127.0.0.1", server.getPort(), cfg);
-    try
-    {
-      Connection connection = factory.getConnection();
-      connection.close();
-    }
-    finally
-    {
-      server.stop();
-    }
-  }
-
-
-
-  /**
-   * Checks that client side IO timeouts are enforced.
-   *
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = true)
-  public void testLDAPConnectionFactoryConnectTimeout() throws Exception
-  {
-    // Mock configuration.
-    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
-        .withConnectionTimeout(500);
-
-    // Mock server.
-    final MockServer server = mockServer(cfg).thenAccept().thenBlock().start();
-
-    // Test connect and close.
-    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
-        "127.0.0.1", server.getPort(), cfg);
-    Connection connection = null;
-    try
-    {
-      connection = factory.getConnection();
-      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
-          SearchFilter.createFilterFromString("(objectClass=*)"));
-      fail("Search attempt should have timed out");
-    }
-    catch (DirectoryException e)
-    {
-      if (e.getResultCode() != ResultCode.CLIENT_SIDE_TIMEOUT)
-      {
-        throw e;
-      }
-    }
-    finally
-    {
-      if (connection != null)
-      {
-        connection.close();
-      }
-      server.unblock();
-      server.stop();
-    }
-  }
-
-
-
-  /**
    * Tests the different mapping policies: connection attempts will succeed, as
    * will any searches, but the final user bind may or may not succeed depending
    * on the provided result code.
@@ -1691,7 +1614,8 @@
       {
         // No valid connections available so this should always fail with
         // INVALID_CREDENTIALS.
-        assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
+        assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+            e.getMessage());
       }
       break;
     }
@@ -1745,6 +1669,565 @@
 
 
 
+  /**
+   * Tests that unknown hosts are handled properly. These should trigger a
+   * CLIENT_SIDE_CONNECT_ERROR result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactoryConnectUnknownHost() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // FIXME: can we guarantee that "unknownhost" does not exist?
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "unknownhost", 31415, cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      fail("Connect attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+          e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+    }
+  }
+
+
+
+  /**
+   * Tests that invalid ports are handled properly. These should trigger a
+   * CLIENT_SIDE_CONNECT_ERROR result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactoryConnectPortNotInUse() throws Exception
+  {
+    // Grab an unused port.
+    ServerSocket socket = TestCaseUtils.bindFreePort();
+    int port = socket.getLocalPort();
+
+    // FIXME: will it matter if the port is left in TIME_WAIT?
+    socket.close();
+
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", port, cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      fail("Connect attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_CONNECT_ERROR,
+          e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+    }
+  }
+
+
+
+  /**
+   * Tests successful connect/unbind.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactoryConnectAndUnbind() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg).thenAccept()
+        .thenReceive(1, new UnbindRequestProtocolOp()).thenClose().start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    try
+    {
+      Connection connection = factory.getConnection();
+      connection.close();
+    }
+    finally
+    {
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search returning a single entry works properly.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchSuccess() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(1, newSearchEntry(adDNString))
+        .thenSend(1, newSearchResult(ResultCode.SUCCESS)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      ByteString username = connection.search(searchBindDN,
+          SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      assertEquals(username, ByteString.valueOf(adDNString));
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search returning no results are handled properly. These should
+   * trigger a CLIENT_SIDE_NO_RESULTS_RETURNED result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchNoResults() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(1, newSearchResult(ResultCode.SUCCESS)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(),
+          ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search returning many results are handled properly. These
+   * should trigger a CLIENT_SIDE_MORE_RESULTS_TO_RETURN result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchTooManyResults() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(1, newSearchEntry(adDNString))
+        .thenSend(1, newSearchEntry(opendjDNString))
+        .thenSend(1, newSearchResult(ResultCode.SUCCESS)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(),
+          ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN, e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search returning a single entry followed by a size limit
+   * exceeded error are handled properly. These should trigger a
+   * CLIENT_SIDE_MORE_RESULTS_TO_RETURN result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchSizeLimit() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(1, newSearchEntry(adDNString))
+        .thenSend(1, newSearchResult(ResultCode.SIZE_LIMIT_EXCEEDED)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(),
+          ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN, e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search which times out at the client. These should trigger a
+   * CLIENT_SIDE_TIMEOUT result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchClientTimeout() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withConnectionTimeout(500);
+
+    // Mock server.
+    final MockServer server = mockServer(cfg).thenAccept().thenBlock().start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(objectClass=*)"));
+      fail("Search attempt should have timed out");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_TIMEOUT,
+          e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.unblock();
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search returning a single entry followed by a time limit
+   * exceeded error are handled properly. These should trigger a
+   * CLIENT_SIDE_MORE_RESULTS_TO_RETURN result code.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchTimeLimit() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(1, newSearchEntry(adDNString))
+        .thenSend(1, newSearchResult(ResultCode.TIME_LIMIT_EXCEEDED)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(), ResultCode.TIME_LIMIT_EXCEEDED,
+          e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search no entries and an error result. The error result code
+   * should be passed back to the called.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchOtherError() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(1, newSearchResult(ResultCode.UNAVAILABLE)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      // Should be the result code sent by the server.
+      assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE, e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search which receives a disconnect notification. The error
+   * result code should be passed back to the called.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchDisconnectNotification()
+      throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenSend(0 /* disconnect ID */,
+            newDisconnectNotification(ResultCode.UNAVAILABLE)).start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      // Should be the result code sent by the server.
+      assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE, e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  /**
+   * Tests valid search which never receives a response because the server
+   * abruptly closes the connection.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLDAPConnectionFactorySearchConnectionClosed()
+      throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg();
+
+    // Mock server.
+    final MockServer server = mockServer(cfg)
+        .thenAccept()
+        .thenReceive(1,
+            newSearchRequest(searchBindDNString, "(uid=aduser)", cfg))
+        .thenClose().start();
+
+    // Test connect and close.
+    final LDAPConnectionFactory factory = new LDAPConnectionFactory(
+        "127.0.0.1", server.getPort(), cfg);
+    Connection connection = null;
+    try
+    {
+      connection = factory.getConnection();
+      connection.search(searchBindDN, SearchScope.WHOLE_SUBTREE,
+          SearchFilter.createFilterFromString("(uid=aduser)"));
+      fail("Search attempt should have failed");
+    }
+    catch (DirectoryException e)
+    {
+      assertEquals(e.getResultCode(), ResultCode.CLIENT_SIDE_SERVER_DOWN,
+          e.getMessage());
+    }
+    finally
+    {
+      if (connection != null)
+      {
+        connection.close();
+      }
+      server.stop();
+    }
+  }
+
+
+
+  // Bind success
+  // Bind authn failure (pwd)
+  // Bind auth failure (username)
+  // Bind auth timeout
+  // Bind auth fatal error
+  // Bind auth non-fatal error
+  // Bind + notice of disconnect
+  // Bind + server close
+
   MockPolicyCfg mockCfg()
   {
     return new MockPolicyCfg();
@@ -1758,4 +2241,39 @@
     final ServerSocket serverSocket = TestCaseUtils.bindFreePort();
     return new MockServer(serverSocket);
   }
+
+
+
+  SearchRequestProtocolOp newSearchRequest(String dn, String filter,
+      LDAPPassThroughAuthenticationPolicyCfg cfg) throws LDAPException
+  {
+    int timeout = (int) (cfg.getConnectionTimeout() / 1000);
+    return new SearchRequestProtocolOp(ByteString.valueOf(dn),
+        SearchScope.WHOLE_SUBTREE, DereferencePolicy.DEREF_ALWAYS, 1, timeout,
+        true, RawFilter.create(filter),
+        LDAPPassThroughAuthenticationPolicyFactory.NO_ATTRIBUTES);
+  }
+
+
+
+  SearchResultEntryProtocolOp newSearchEntry(String dn)
+      throws DirectoryException
+  {
+    return new SearchResultEntryProtocolOp(DN.decode(dn));
+  }
+
+
+
+  SearchResultDoneProtocolOp newSearchResult(ResultCode resultCode)
+  {
+    return new SearchResultDoneProtocolOp(resultCode.getIntValue());
+  }
+
+
+
+  ExtendedResponseProtocolOp newDisconnectNotification(ResultCode resultCode)
+  {
+    return new ExtendedResponseProtocolOp(resultCode.getIntValue(), null, null,
+        null, OID_NOTICE_OF_DISCONNECTION, null);
+  }
 }

--
Gitblit v1.10.0