From 532acbf84c0d79da39a50fb74d55001a05d23852 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Tue, 06 Sep 2011 16:21:18 +0000
Subject: [PATCH] Issue OPENDJ-262: Implement pass through authentication (PTA)

---
 opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java                          |  596 ++++++++++++++++++++++++++++++++++++++++++++-
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java |  137 ++++------
 2 files changed, 637 insertions(+), 96 deletions(-)

diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 106cf5d..f5ec6b7 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -34,14 +34,17 @@
 import java.io.Closeable;
 import java.util.LinkedList;
 import java.util.List;
+import java.util.Set;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.Semaphore;
+import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock;
 
 import org.opends.messages.Message;
 import org.opends.server.admin.server.ConfigurationChangeListener;
-import org.opends.server.admin.std.server.
-    LDAPPassThroughAuthenticationPolicyCfg;
+import org.opends.server.admin.std.server.*;
 import org.opends.server.api.AuthenticationPolicy;
 import org.opends.server.api.AuthenticationPolicyFactory;
 import org.opends.server.api.AuthenticationPolicyState;
@@ -58,6 +61,11 @@
     AuthenticationPolicyFactory<LDAPPassThroughAuthenticationPolicyCfg>
 {
 
+  // TODO: retry operations transparently until all connections exhausted.
+  // TODO: handle password policy response controls? AD?
+  // TODO: periodically ping offline servers in order to detect when they come
+  // back.
+
   /**
    * An LDAP connection which will be used in order to search for or
    * authenticate users.
@@ -75,9 +83,9 @@
 
     /**
      * Returns the name of the user whose entry matches the provided search
-     * criteria.
-     * <p>
-     * TODO: define result codes used when no entries found or too many entries.
+     * criteria. This will return CLIENT_SIDE_NO_RESULTS_RETURNED/NO_SUCH_OBJECT
+     * if no search results were returned, or CLIENT_SIDE_MORE_RESULTS_TO_RETURN
+     * if too many results were returned.
      *
      * @param baseDN
      *          The search base DN.
@@ -165,6 +173,483 @@
   private final class PolicyImpl extends AuthenticationPolicy implements
       ConfigurationChangeListener<LDAPPassThroughAuthenticationPolicyCfg>
   {
+
+    /**
+     * A factory which returns pre-authenticated connections for searches.
+     */
+    private final class AuthenticatedConnectionFactory implements
+        ConnectionFactory
+    {
+
+      private final ConnectionFactory factory;
+
+
+
+      private AuthenticatedConnectionFactory(final ConnectionFactory factory)
+      {
+        this.factory = factory;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        final DN username = configuration.getMappedSearchBindDN();
+        final String password = configuration.getMappedSearchBindPassword();
+
+        final Connection connection = factory.getConnection();
+        if (username != null && !username.isNullDN())
+        {
+          try
+          {
+            connection.simpleBind(ByteString.valueOf(username.toString()),
+                ByteString.valueOf(password));
+          }
+          catch (final DirectoryException e)
+          {
+            connection.close();
+            throw e;
+          }
+        }
+        return connection;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("AuthenticationConnectionFactory(");
+        builder.append(factory);
+        builder.append(')');
+        return builder.toString();
+      }
+
+    }
+
+
+
+    /**
+     * PTA connection pool.
+     */
+    private final class ConnectionPool implements ConnectionFactory, Closeable
+    {
+
+      /**
+       * Pooled connection's intercept close and release connection back to the
+       * pool.
+       */
+      private final class PooledConnection implements Connection
+      {
+        private final Connection connection;
+        private boolean connectionIsClosed = false;
+
+
+
+        private PooledConnection(final Connection connection)
+        {
+          this.connection = connection;
+        }
+
+
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void close()
+        {
+          if (!connectionIsClosed)
+          {
+            connectionIsClosed = true;
+
+            // Guarded by PolicyImpl
+            if (poolIsClosed)
+            {
+              connection.close();
+            }
+            else
+            {
+              pooledConnections.offer(this);
+            }
+            availableConnections.release();
+          }
+        }
+
+
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public ByteString search(final DN baseDN, final SearchScope scope,
+            final SearchFilter filter) throws DirectoryException
+        {
+          try
+          {
+            return connection.search(baseDN, scope, filter);
+          }
+          catch (final DirectoryException e)
+          {
+            // Don't put the connection back in the pool if it has failed.
+            closeConnectionOnFatalError(e);
+            throw e;
+          }
+        }
+
+
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void simpleBind(final ByteString username,
+            final ByteString password) throws DirectoryException
+        {
+          try
+          {
+            connection.simpleBind(username, password);
+          }
+          catch (final DirectoryException e)
+          {
+            // Don't put the connection back in the pool if it has failed.
+            closeConnectionOnFatalError(e);
+            throw e;
+          }
+        }
+
+
+
+        private void closeConnectionOnFatalError(final DirectoryException e)
+        {
+          if (isFatalResultCode(e.getResultCode()))
+          {
+            connectionIsClosed = true;
+            connection.close();
+            availableConnections.release();
+          }
+        }
+
+      }
+
+
+
+      // Guarded by PolicyImpl.lock.
+      private boolean poolIsClosed = false;
+
+      private final ConnectionFactory factory;
+      private final int poolSize =
+        Runtime.getRuntime().availableProcessors() * 2;
+      private final Semaphore availableConnections = new Semaphore(poolSize);
+      private final LinkedBlockingQueue<PooledConnection> pooledConnections =
+        new LinkedBlockingQueue<PooledConnection>();
+
+
+
+      private ConnectionPool(final ConnectionFactory factory)
+      {
+        this.factory = factory;
+      }
+
+
+
+      /**
+       * Release all connections: do we want to block?
+       */
+      @Override
+      public void close()
+      {
+        // No need for synchronization as this can only be called with the
+        // policy's exclusive lock.
+        poolIsClosed = true;
+
+        PooledConnection pooledConnection;
+        while ((pooledConnection = pooledConnections.poll()) != null)
+        {
+          pooledConnection.connection.close();
+        }
+
+        // Since we have the exclusive lock, there should be no more connections
+        // in use.
+        if (availableConnections.availablePermits() != poolSize)
+        {
+          throw new IllegalStateException(
+              "Pool has remaining connections open after close");
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        // This should only be called with the policy's shared lock.
+        if (poolIsClosed)
+        {
+          throw new IllegalStateException("pool is closed");
+        }
+
+        availableConnections.acquireUninterruptibly();
+
+        // There is either a pooled connection or we are allowed to create
+        // one.
+        PooledConnection pooledConnection = pooledConnections.poll();
+        if (pooledConnection == null)
+        {
+          try
+          {
+            final Connection connection = factory.getConnection();
+            pooledConnection = new PooledConnection(connection);
+          }
+          catch (final DirectoryException e)
+          {
+            availableConnections.release();
+            throw e;
+          }
+        }
+
+        return pooledConnection;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("ConnectionPool(");
+        builder.append(factory);
+        builder.append(", poolSize=");
+        builder.append(poolSize);
+        builder.append(", inPool=");
+        builder.append(pooledConnections.size());
+        builder.append(", available=");
+        builder.append(availableConnections.availablePermits());
+        builder.append(')');
+        return builder.toString();
+      }
+    }
+
+
+
+    /**
+     * A simplistic two-way fail-over connection factory implementation.
+     */
+    private final class FailoverConnectionFactory implements ConnectionFactory,
+        Closeable
+    {
+      private final LoadBalancer primary;
+      private final LoadBalancer secondary;
+
+
+
+      private FailoverConnectionFactory(final LoadBalancer primary,
+          final LoadBalancer secondary)
+      {
+        this.primary = primary;
+        this.secondary = secondary;
+      }
+
+
+
+      /**
+       * Close underlying load-balancers.
+       */
+      @Override
+      public void close()
+      {
+        primary.close();
+        if (secondary != null)
+        {
+          secondary.close();
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        if (secondary == null)
+        {
+          // No fail-over so just use the primary.
+          return primary.getConnection();
+        }
+        else
+        {
+          try
+          {
+            return primary.getConnection();
+          }
+          catch (final DirectoryException e)
+          {
+            return secondary.getConnection();
+          }
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("FailoverConnectionFactory(");
+        builder.append(primary);
+        builder.append(", ");
+        builder.append(secondary);
+        builder.append(')');
+        return builder.toString();
+      }
+
+    }
+
+
+
+    /**
+     * A simplistic load-balancer connection factory implementation using
+     * approximately round-robin balancing.
+     */
+    private final class LoadBalancer implements ConnectionFactory, Closeable
+    {
+      private final ConnectionPool[] factories;
+      private final AtomicInteger nextIndex = new AtomicInteger();
+      private final int maxIndex;
+
+
+
+      private LoadBalancer(final ConnectionPool[] factories)
+      {
+        this.factories = factories;
+        this.maxIndex = factories.length;
+      }
+
+
+
+      /**
+       * Close underlying connection pools.
+       */
+      @Override
+      public void close()
+      {
+        for (final ConnectionPool pool : factories)
+        {
+          pool.close();
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        final int startIndex = getStartIndex();
+        int index = startIndex;
+        for (;;)
+        {
+          final ConnectionFactory factory = factories[index];
+
+          try
+          {
+            return factory.getConnection();
+          }
+          catch (final DirectoryException e)
+          {
+            // Try the next index.
+            if (++index == maxIndex)
+            {
+              index = 0;
+            }
+
+            // If all the factories have been tried then give up and throw the
+            // exception.
+            if (index == startIndex)
+            {
+              throw e;
+            }
+          }
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public String toString()
+      {
+        final StringBuilder builder = new StringBuilder();
+        builder.append("LoadBalancer(");
+        builder.append(nextIndex);
+        for (final ConnectionFactory factory : factories)
+        {
+          builder.append(", ");
+          builder.append(factory);
+        }
+        builder.append(')');
+        return builder.toString();
+      }
+
+
+
+      // Determine the start index.
+      private int getStartIndex()
+      {
+        // A round robin pool of one connection factories is unlikely in
+        // practice and requires special treatment.
+        if (maxIndex == 1)
+        {
+          return 0;
+        }
+
+        // Determine the next factory to use: avoid blocking algorithm.
+        int oldNextIndex;
+        int newNextIndex;
+        do
+        {
+          oldNextIndex = nextIndex.get();
+          newNextIndex = oldNextIndex + 1;
+          if (newNextIndex == maxIndex)
+          {
+            newNextIndex = 0;
+          }
+        }
+        while (!nextIndex.compareAndSet(oldNextIndex, newNextIndex));
+
+        // There's a potential, but benign, race condition here: other threads
+        // could jump in and rotate through the list before we return the
+        // connection factory.
+        return newNextIndex;
+      }
+
+    }
+
+
+
     /**
      * LDAP PTA policy state implementation.
      */
@@ -329,8 +814,6 @@
               {
                 switch (e.getResultCode())
                 {
-                // FIXME: specify possible result codes. What about authz
-                // errors?
                 case NO_SUCH_OBJECT:
                 case CLIENT_SIDE_NO_RESULTS_RETURNED:
                   // Ignore and try next base DN.
@@ -389,7 +872,6 @@
           {
             switch (e.getResultCode())
             {
-            // FIXME: specify possible result codes.
             case NO_SUCH_OBJECT:
             case INVALID_CREDENTIALS:
               return false;
@@ -430,9 +912,8 @@
     // Current configuration.
     private LDAPPassThroughAuthenticationPolicyCfg configuration;
 
-    // FIXME: initialize connection factories.
-    private ConnectionFactory searchFactory = null;
-    private ConnectionFactory bindFactory = null;
+    private FailoverConnectionFactory searchFactory = null;
+    private FailoverConnectionFactory bindFactory = null;
 
 
 
@@ -529,7 +1010,18 @@
       exclusiveLock.lock();
       try
       {
-        // TODO: close all connections.
+        if (searchFactory != null)
+        {
+          searchFactory.close();
+          searchFactory = null;
+        }
+
+        if (bindFactory != null)
+        {
+          bindFactory.close();
+          bindFactory = null;
+        }
+
       }
       finally
       {
@@ -544,11 +1036,54 @@
     {
       this.configuration = configuration;
 
-      // TODO: implement FO/LB/CP + authenticated search factory.
-      final String hostPort = configuration.getPrimaryRemoteLDAPServer()
-          .first();
-      searchFactory = newLDAPConnectionFactory(hostPort);
-      bindFactory = newLDAPConnectionFactory(hostPort);
+      // Create load-balancers for primary servers.
+      final LoadBalancer primarySearchLoadBalancer;
+      final LoadBalancer primaryBindLoadBalancer;
+
+      Set<String> servers = configuration.getPrimaryRemoteLDAPServer();
+      ConnectionPool[] searchPool = new ConnectionPool[servers.size()];
+      ConnectionPool[] bindPool = new ConnectionPool[servers.size()];
+      int index = 0;
+      for (final String hostPort : servers)
+      {
+        final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
+        searchPool[index] = new ConnectionPool(
+            new AuthenticatedConnectionFactory(factory));
+        bindPool[index++] = new ConnectionPool(factory);
+      }
+      primarySearchLoadBalancer = new LoadBalancer(searchPool);
+      primaryBindLoadBalancer = new LoadBalancer(bindPool);
+
+      // Create load-balancers for secondary servers.
+      final LoadBalancer secondarySearchLoadBalancer;
+      final LoadBalancer secondaryBindLoadBalancer;
+
+      servers = configuration.getSecondaryRemoteLDAPServer();
+      if (servers.isEmpty())
+      {
+        secondarySearchLoadBalancer = null;
+        secondaryBindLoadBalancer = null;
+      }
+      else
+      {
+        searchPool = new ConnectionPool[servers.size()];
+        bindPool = new ConnectionPool[servers.size()];
+        index = 0;
+        for (final String hostPort : servers)
+        {
+          final ConnectionFactory factory = newLDAPConnectionFactory(hostPort);
+          searchPool[index] = new ConnectionPool(
+              new AuthenticatedConnectionFactory(factory));
+          bindPool[index++] = new ConnectionPool(factory);
+        }
+        secondarySearchLoadBalancer = new LoadBalancer(searchPool);
+        secondaryBindLoadBalancer = new LoadBalancer(bindPool);
+      }
+
+      searchFactory = new FailoverConnectionFactory(primarySearchLoadBalancer,
+          secondarySearchLoadBalancer);
+      bindFactory = new FailoverConnectionFactory(primaryBindLoadBalancer,
+          secondaryBindLoadBalancer);
     }
 
 
@@ -589,6 +1124,33 @@
 
 
   /**
+   * Determines whether or no a result code is expected to trigger the
+   * associated connection to be closed immediately.
+   *
+   * @param resultCode
+   *          The result code.
+   * @return {@code true} if the result code is expected to trigger the
+   *         associated connection to be closed immediately.
+   */
+  static boolean isFatalResultCode(final ResultCode resultCode)
+  {
+    switch (resultCode)
+    {
+    case BUSY:
+    case UNAVAILABLE:
+    case PROTOCOL_ERROR:
+    case OTHER:
+    case UNWILLING_TO_PERFORM:
+    case OPERATIONS_ERROR:
+      return true;
+    default:
+      return false;
+    }
+  }
+
+
+
+  /**
    * Public default constructor used by the admin framework. This will use the
    * default LDAP connection factory provider.
    */
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
index eea668a..1d02578 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
+++ b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -28,6 +28,7 @@
 
 
 
+import static org.opends.server.extensions.LDAPPassThroughAuthenticationPolicyFactory.isFatalResultCode;
 import static org.testng.Assert.assertEquals;
 import static org.testng.Assert.assertFalse;
 import static org.testng.Assert.assertTrue;
@@ -309,7 +310,7 @@
     public void close()
     {
       CloseEvent event = new CloseEvent(getConnectionEvent);
-      mockProvider.assertNextEventExpected(event);
+      mockProvider.assertExpectedEventWasReceived(event);
     }
 
 
@@ -322,10 +323,10 @@
     {
       SearchEvent event = new SearchEvent(getConnectionEvent,
           baseDN.toString(), scope, filter.toString());
-      Object result = mockProvider.assertNextEventExpected(event);
-      if (result instanceof ByteString)
+      Object result = mockProvider.assertExpectedEventWasReceived(event);
+      if (result instanceof String)
       {
-        return (ByteString) result;
+        return ByteString.valueOf((String) result);
       }
       else
       {
@@ -343,7 +344,7 @@
     {
       SimpleBindEvent event = new SimpleBindEvent(getConnectionEvent,
           username.toString(), password.toString());
-      DirectoryException e = mockProvider.assertNextEventExpected(event);
+      DirectoryException e = mockProvider.assertExpectedEventWasReceived(event);
       if (e != null) throw e;
     }
 
@@ -376,7 +377,7 @@
       GetConnectionEvent event = new GetConnectionEvent(
           getLDAPConnectionFactoryEvent);
 
-      DirectoryException e = mockProvider.assertNextEventExpected(event);
+      DirectoryException e = mockProvider.assertExpectedEventWasReceived(event);
       if (e != null)
       {
         throw e;
@@ -624,14 +625,14 @@
     {
       GetLDAPConnectionFactoryEvent event = new GetLDAPConnectionFactoryEvent(
           host + ":" + port, options);
-      assertNextEventExpected(event);
+      assertExpectedEventWasReceived(event);
       return new MockFactory(this, event);
     }
 
 
 
     @SuppressWarnings("unchecked")
-    <T> T assertNextEventExpected(Event<T> actualEvent)
+    <T> T assertExpectedEventWasReceived(Event<T> actualEvent)
     {
       Event<?> expectedEvent = expectedEvents.poll();
       if (expectedEvent == null)
@@ -647,7 +648,7 @@
 
 
 
-    MockProvider withExpectedEvent(Event<?> expectedEvent)
+    MockProvider expectEvent(Event<?> expectedEvent)
     {
       expectedEvents.add(expectedEvent);
       return this;
@@ -655,7 +656,7 @@
 
 
 
-    void assertNoMoreEvents()
+    void assertAllExpectedEventsReceived()
     {
       assertTrue(expectedEvents.isEmpty());
     }
@@ -928,7 +929,7 @@
    * @throws Exception
    *           If an unexpected exception occurred.
    */
-  @Test(enabled = false, dataProvider = "testConnectionFailureDuringSearchGetConnectionData")
+  @Test(enabled = true, dataProvider = "testConnectionFailureDuringSearchGetConnectionData")
   public void testConnectionFailureDuringSearchGetConnection(
       ResultCode connectResultCode) throws Exception
   {
@@ -943,8 +944,8 @@
         phost1, cfg);
     GetConnectionEvent ceSearch = new GetConnectionEvent(fe, connectResultCode);
 
-    MockProvider provider = new MockProvider().withExpectedEvent(fe)
-        .withExpectedEvent(ceSearch);
+    MockProvider provider = new MockProvider().expectEvent(fe).expectEvent(
+        ceSearch);
 
     // Obtain policy and state.
     LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
@@ -964,12 +965,12 @@
     catch (DirectoryException e)
     {
       // No valid connections available so this should always fail with
-      // UNAVAILABLE.
-      assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
+      // INVALID_CREDENTIALS.
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
     }
 
     // Tear down and check final state.
-    provider.assertNoMoreEvents();
+    provider.assertAllExpectedEventsReceived();
     state.finalizeStateAfterBind();
     policy.finalizeAuthenticationPolicy();
   }
@@ -1008,7 +1009,7 @@
    * @throws Exception
    *           If an unexpected exception occurred.
    */
-  @Test(enabled = false, dataProvider = "testConnectionFailureDuringSearchBindData")
+  @Test(enabled = true, dataProvider = "testConnectionFailureDuringSearchBindData")
   public void testConnectionFailureDuringSearchBind(ResultCode bindResultCode)
       throws Exception
   {
@@ -1024,11 +1025,11 @@
     GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
 
     MockProvider provider = new MockProvider()
-        .withExpectedEvent(fe)
-        .withExpectedEvent(ceSearch)
-        .withExpectedEvent(
+        .expectEvent(fe)
+        .expectEvent(ceSearch)
+        .expectEvent(
             new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
-                bindResultCode)).withExpectedEvent(new CloseEvent(ceSearch));
+                bindResultCode)).expectEvent(new CloseEvent(ceSearch));
 
     // Obtain policy and state.
     LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
@@ -1048,12 +1049,12 @@
     catch (DirectoryException e)
     {
       // No valid connections available so this should always fail with
-      // UNAVAILABLE.
-      assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
+      // INVALID_CREDENTIALS.
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
     }
 
     // Tear down and check final state.
-    provider.assertNoMoreEvents();
+    provider.assertAllExpectedEventsReceived();
     state.finalizeStateAfterBind();
     policy.finalizeAuthenticationPolicy();
   }
@@ -1071,6 +1072,8 @@
     // @formatter:off
     return new Object[][] {
         { ResultCode.NO_SUCH_OBJECT },
+        { ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED },
+        { ResultCode.CLIENT_SIDE_MORE_RESULTS_TO_RETURN },
         { ResultCode.UNAVAILABLE }
     };
     // @formatter:on
@@ -1089,7 +1092,7 @@
    * @throws Exception
    *           If an unexpected exception occurred.
    */
-  @Test(enabled = false, dataProvider = "testConnectionFailureDuringSearchData")
+  @Test(enabled = true, dataProvider = "testConnectionFailureDuringSearchData")
   public void testConnectionFailureDuringSearch(ResultCode searchResultCode)
       throws Exception
   {
@@ -1105,18 +1108,18 @@
     GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
 
     MockProvider provider = new MockProvider()
-        .withExpectedEvent(fe)
-        .withExpectedEvent(ceSearch)
-        .withExpectedEvent(
+        .expectEvent(fe)
+        .expectEvent(ceSearch)
+        .expectEvent(
             new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
                 ResultCode.SUCCESS))
-        .withExpectedEvent(
+        .expectEvent(
             new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
                 "(uid=aduser)", searchResultCode));
     if (isFatalResultCode(searchResultCode))
     {
       // The connection will fail and be closed immediately.
-      provider.withExpectedEvent(new CloseEvent(ceSearch));
+      provider.expectEvent(new CloseEvent(ceSearch));
     }
 
     // Obtain policy and state.
@@ -1137,23 +1140,23 @@
     catch (DirectoryException e)
     {
       // No valid connections available so this should always fail with
-      // UNAVAILABLE.
-      assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
+      // INVALID_CREDENTIALS.
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
     }
 
     // There should be no more pending events.
-    provider.assertNoMoreEvents();
+    provider.assertAllExpectedEventsReceived();
     state.finalizeStateAfterBind();
 
     // Cached connections should be closed when the policy is finalized.
     if (!isFatalResultCode(searchResultCode))
     {
-      provider.withExpectedEvent(new CloseEvent(ceSearch));
+      provider.expectEvent(new CloseEvent(ceSearch));
     }
 
     // Tear down and check final state.
     policy.finalizeAuthenticationPolicy();
-    provider.assertNoMoreEvents();
+    provider.assertAllExpectedEventsReceived();
   }
 
 
@@ -1168,7 +1171,7 @@
   {
     // @formatter:off
     return new Object[][] {
-        /* policy, connection failure, invalid credentials */
+        /* policy, bind result code */
         { MappingPolicy.UNMAPPED, ResultCode.SUCCESS },
         { MappingPolicy.UNMAPPED, ResultCode.INVALID_CREDENTIALS },
         { MappingPolicy.UNMAPPED, ResultCode.UNAVAILABLE },
@@ -1201,7 +1204,7 @@
    * @throws Exception
    *           If an unexpected exception occurred.
    */
-  @Test(enabled = false, dataProvider = "testMappingPolicyAuthenticationData")
+  @Test(enabled = true, dataProvider = "testMappingPolicyAuthenticationData")
   public void testMappingPolicyAuthentication(MappingPolicy mappingPolicy,
       ResultCode bindResultCode) throws Exception
   {
@@ -1216,31 +1219,34 @@
     // Create the provider and its list of expected events.
     GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
         phost1, cfg);
-    MockProvider provider = new MockProvider().withExpectedEvent(fe);
+    MockProvider provider = new MockProvider().expectEvent(fe);
 
     // Add search events if doing a mapped search.
     GetConnectionEvent ceSearch = null;
     if (mappingPolicy == MappingPolicy.MAPPED_SEARCH)
     {
       ceSearch = new GetConnectionEvent(fe);
-      provider.withExpectedEvent(
-          new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
-              ResultCode.SUCCESS)).withExpectedEvent(
-          new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
-              "(uid=aduser)", adDNString));
+      provider
+          .expectEvent(ceSearch)
+          .expectEvent(
+              new SimpleBindEvent(ceSearch, searchBindDNString,
+                  "searchPassword", ResultCode.SUCCESS))
+          .expectEvent(
+              new SearchEvent(ceSearch, "o=ad", SearchScope.WHOLE_SUBTREE,
+                  "(uid=aduser)", adDNString));
       // Connection should be cached until the policy is finalized.
     }
 
     // Add bind events.
     GetConnectionEvent ceBind = new GetConnectionEvent(fe);
-    provider.withExpectedEvent(ceBind).withExpectedEvent(
+    provider.expectEvent(ceBind).expectEvent(
         new SimpleBindEvent(ceBind,
             mappingPolicy == MappingPolicy.UNMAPPED ? opendjDNString
                 : adDNString, userPassword, bindResultCode));
     if (isFatalResultCode(bindResultCode))
     {
       // The connection will fail and be closed immediately.
-      provider.withExpectedEvent(new CloseEvent(ceBind));
+      provider.expectEvent(new CloseEvent(ceBind));
     }
 
     // Connection should be cached until the policy is finalized or until the
@@ -1272,57 +1278,30 @@
       }
       catch (DirectoryException e)
       {
-        // No authentication related error codes should be mapped to
-        // UNAVAILABLE.
-        assertEquals(e.getResultCode(), ResultCode.UNAVAILABLE);
+        // No valid connections available so this should always fail with
+        // INVALID_CREDENTIALS.
+        assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS);
       }
       break;
     }
 
     // There should be no more pending events.
-    provider.assertNoMoreEvents();
+    provider.assertAllExpectedEventsReceived();
     state.finalizeStateAfterBind();
 
     // Cached connections should be closed when the policy is finalized.
     if (ceSearch != null)
     {
-      provider.withExpectedEvent(new CloseEvent(ceSearch));
+      provider.expectEvent(new CloseEvent(ceSearch));
     }
     if (!isFatalResultCode(bindResultCode))
     {
-      provider.withExpectedEvent(new CloseEvent(ceBind));
+      provider.expectEvent(new CloseEvent(ceBind));
     }
 
     // Tear down and check final state.
     policy.finalizeAuthenticationPolicy();
-    provider.assertNoMoreEvents();
-  }
-
-
-
-  /**
-   * Determines whether or no a result code is expected to trigger the
-   * associated connection to be closed immediately.
-   *
-   * @param resultCode
-   *          The result code.
-   * @return {@code true} if the result code is expected to trigger the
-   *         associated connection to be closed immediately.
-   */
-  private boolean isFatalResultCode(ResultCode resultCode)
-  {
-    switch (resultCode)
-    {
-    case BUSY:
-    case UNAVAILABLE:
-    case PROTOCOL_ERROR:
-    case OTHER:
-    case UNWILLING_TO_PERFORM:
-    case OPERATIONS_ERROR:
-      return true;
-    default:
-      return false;
-    }
+    provider.assertAllExpectedEventsReceived();
   }
 
 

--
Gitblit v1.10.0