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

---
 opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java | 1581 +++++++++++++++++++++++++------------------
 opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java                          |  555 +++++++++++---
 2 files changed, 1,346 insertions(+), 790 deletions(-)

diff --git a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 6283310..1b32e4c 100644
--- a/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -37,8 +37,7 @@
 import java.io.IOException;
 import java.net.*;
 import java.util.*;
-import java.util.concurrent.ConcurrentLinkedQueue;
-import java.util.concurrent.Semaphore;
+import java.util.concurrent.*;
 import java.util.concurrent.atomic.AtomicInteger;
 import java.util.concurrent.locks.ReentrantReadWriteLock;
 import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock;
@@ -49,10 +48,7 @@
 import org.opends.messages.Message;
 import org.opends.server.admin.server.ConfigurationChangeListener;
 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;
-import org.opends.server.api.TrustManagerProvider;
+import org.opends.server.api.*;
 import org.opends.server.config.ConfigException;
 import org.opends.server.core.DirectoryServer;
 import org.opends.server.loggers.debug.DebugLogger;
@@ -72,11 +68,353 @@
     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.
   // TODO: provide alternative cfg for search password.
+  // TODO: custom aliveness pings
+  // TODO: manage account lockout
+  // TODO: cache password
+
+  /**
+   * A simplistic load-balancer connection factory implementation using
+   * approximately round-robin balancing.
+   */
+  static abstract class AbstractLoadBalancer implements ConnectionFactory,
+      Runnable
+  {
+    /**
+     * A connection which automatically retries operations on other servers.
+     */
+    private final class FailoverConnection implements Connection
+    {
+      private Connection connection;
+      private MonitoredConnectionFactory factory;
+      private final int startIndex;
+      private int nextIndex;
+
+
+
+      private FailoverConnection(final int startIndex)
+          throws DirectoryException
+      {
+        this.startIndex = nextIndex = startIndex;
+
+        DirectoryException lastException = null;
+        do
+        {
+          factory = factories[nextIndex];
+          if (factory.isAvailable())
+          {
+            try
+            {
+              if (factory.isAvailable)
+              {
+                connection = factory.getConnection();
+                incrementNextIndex();
+                return;
+              }
+            }
+            catch (final DirectoryException e)
+            {
+              // Ignore this error and try the next factory.
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, e);
+              }
+              lastException = e;
+            }
+          }
+          incrementNextIndex();
+        }
+        while (nextIndex != startIndex);
+
+        // All the factories have been tried so give up and throw the exception.
+        throw lastException;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void close()
+      {
+        connection.close();
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public ByteString search(final DN baseDN, final SearchScope scope,
+          final SearchFilter filter) throws DirectoryException
+      {
+        for (;;)
+        {
+          try
+          {
+            return connection.search(baseDN, scope, filter);
+          }
+          catch (final DirectoryException e)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, e);
+            }
+            handleDirectoryException(e);
+          }
+        }
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void simpleBind(final ByteString username,
+          final ByteString password) throws DirectoryException
+      {
+        for (;;)
+        {
+          try
+          {
+            connection.simpleBind(username, password);
+            return;
+          }
+          catch (final DirectoryException e)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, e);
+            }
+            handleDirectoryException(e);
+          }
+        }
+      }
+
+
+
+      private void handleDirectoryException(final DirectoryException e)
+          throws DirectoryException
+      {
+        // If the error does not indicate that the connection has failed, then
+        // pass this back to the caller.
+        if (!isFatalResultCode(e.getResultCode()))
+        {
+          throw e;
+        }
+
+        // The associated server is unavailable, so close the connection and
+        // try the next connection factory.
+        connection.close();
+        factory.isAvailable = false;
+
+        while (nextIndex != startIndex)
+        {
+          factory = factories[nextIndex];
+          if (factory.isAvailable())
+          {
+            try
+            {
+              if (factory.isAvailable)
+              {
+                connection = factory.getConnection();
+                incrementNextIndex();
+                return;
+              }
+            }
+            catch (final DirectoryException de)
+            {
+              // Ignore this error and try the next factory.
+              if (debugEnabled())
+              {
+                TRACER.debugCaught(DebugLogLevel.ERROR, de);
+              }
+            }
+          }
+          incrementNextIndex();
+        }
+
+        // All the factories have been tried so give up and throw the exception.
+        throw e;
+      }
+
+
+
+      private void incrementNextIndex()
+      {
+        // Try the next index.
+        if (++nextIndex == maxIndex)
+        {
+          nextIndex = 0;
+        }
+      }
+
+    }
+
+
+
+    /**
+     * A connection factory which caches its online/offline state in order to
+     * avoid unnecessary connection attempts when it is known to be offline.
+     */
+    private final class MonitoredConnectionFactory implements ConnectionFactory
+    {
+      private final ConnectionFactory factory;
+      private volatile boolean isAvailable = true;
+
+
+
+      private MonitoredConnectionFactory(final ConnectionFactory factory)
+      {
+        this.factory = factory;
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public void close()
+      {
+        factory.close();
+      }
+
+
+
+      /**
+       * {@inheritDoc}
+       */
+      @Override
+      public Connection getConnection() throws DirectoryException
+      {
+        try
+        {
+          final Connection connection = factory.getConnection();
+          isAvailable = true;
+          return connection;
+        }
+        catch (final DirectoryException e)
+        {
+          if (debugEnabled())
+          {
+            TRACER.debugCaught(DebugLogLevel.ERROR, e);
+          }
+          isAvailable = false;
+          throw e;
+        }
+      }
+
+
+
+      private boolean isAvailable()
+      {
+        return isAvailable;
+      }
+    }
+
+
+
+    private final MonitoredConnectionFactory[] factories;
+    private final int maxIndex;
+    private final ScheduledFuture<?> monitorFuture;
+
+
+
+    /**
+     * Creates a new abstract load-balancer.
+     *
+     * @param factories
+     *          The list of underlying connection factories.
+     * @param scheduler
+     *          The monitoring scheduler.
+     */
+    AbstractLoadBalancer(final ConnectionFactory[] factories,
+        final ScheduledExecutorService scheduler)
+    {
+      this.factories = new MonitoredConnectionFactory[factories.length];
+      this.maxIndex = factories.length;
+
+      for (int i = 0; i < maxIndex; i++)
+      {
+        this.factories[i] = new MonitoredConnectionFactory(factories[i]);
+      }
+
+      this.monitorFuture = scheduler.scheduleWithFixedDelay(this, 5, 5,
+          TimeUnit.SECONDS);
+    }
+
+
+
+    /**
+     * Close underlying connection pools.
+     */
+    @Override
+    public final void close()
+    {
+      monitorFuture.cancel(true);
+
+      for (final ConnectionFactory factory : factories)
+      {
+        factory.close();
+      }
+    }
+
+
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public final Connection getConnection() throws DirectoryException
+    {
+      final int startIndex = getStartIndex();
+      return new FailoverConnection(startIndex);
+    }
+
+
+
+    /**
+     * Try to connect to any offline connection factories.
+     */
+    @Override
+    public void run()
+    {
+      for (final MonitoredConnectionFactory factory : factories)
+      {
+        if (!factory.isAvailable())
+        {
+          try
+          {
+            factory.getConnection().close();
+          }
+          catch (final DirectoryException e)
+          {
+            if (debugEnabled())
+            {
+              TRACER.debugCaught(DebugLogLevel.ERROR, e);
+            }
+          }
+        }
+      }
+    }
+
+
+
+    /**
+     * Return the start which should be used for the next connection attempt.
+     *
+     * @return The start which should be used for the next connection attempt.
+     */
+    abstract int getStartIndex();
+
+  }
+
+
 
   /**
    * A factory which returns pre-authenticated connections for searches.
@@ -245,7 +583,7 @@
    * <p>
    * Package private for testing.
    */
-  static final class ConnectionPool implements ConnectionFactory, Closeable
+  static final class ConnectionPool implements ConnectionFactory
   {
 
     /**
@@ -445,13 +783,8 @@
    * <p>
    * Package private for testing.
    */
-  static final class FailoverConnectionFactory implements ConnectionFactory,
-      Closeable
+  static final class FailoverLoadBalancer extends AbstractLoadBalancer
   {
-    private final ConnectionFactory primary;
-    private final ConnectionFactory secondary;
-
-
 
     /**
      * Creates a new fail-over connection factory which will always try the
@@ -461,27 +794,14 @@
      *          The primary connection factory.
      * @param secondary
      *          The secondary connection factory.
+     * @param scheduler
+     *          The monitoring scheduler.
      */
-    FailoverConnectionFactory(final ConnectionFactory primary,
-        final ConnectionFactory secondary)
+    FailoverLoadBalancer(final ConnectionFactory primary,
+        final ConnectionFactory secondary,
+        final ScheduledExecutorService scheduler)
     {
-      this.primary = primary;
-      this.secondary = secondary;
-    }
-
-
-
-    /**
-     * Close underlying load-balancers.
-     */
-    @Override
-    public void close()
-    {
-      primary.close();
-      if (secondary != null)
-      {
-        secondary.close();
-      }
+      super(new ConnectionFactory[] { primary, secondary }, scheduler);
     }
 
 
@@ -490,24 +810,10 @@
      * {@inheritDoc}
      */
     @Override
-    public Connection getConnection() throws DirectoryException
+    int getStartIndex()
     {
-      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();
-        }
-      }
+      // Always start with the primaries.
+      return 0;
     }
 
   }
@@ -1116,9 +1422,9 @@
 
   /**
    * An interface for obtaining a connection factory for LDAP connections to a
-   * named LDAP server.
+   * named LDAP server and the monitoring scheduler.
    */
-  static interface LDAPConnectionFactoryProvider
+  static interface Provider
   {
     /**
      * Returns a connection factory which can be used for obtaining connections
@@ -1135,6 +1441,17 @@
      */
     ConnectionFactory getLDAPConnectionFactory(String host, int port,
         LDAPPassThroughAuthenticationPolicyCfg cfg);
+
+
+
+    /**
+     * Returns the scheduler which should be used to periodically ping
+     * connection factories to determine when they are online.
+     *
+     * @return The scheduler which should be used to periodically ping
+     *         connection factories to determine when they are online.
+     */
+    ScheduledExecutorService getScheduledExecutorService();
   }
 
 
@@ -1143,9 +1460,8 @@
    * A simplistic load-balancer connection factory implementation using
    * approximately round-robin balancing.
    */
-  static final class LoadBalancer implements ConnectionFactory, Closeable
+  static final class RoundRobinLoadBalancer extends AbstractLoadBalancer
   {
-    private final ConnectionFactory[] factories;
     private final AtomicInteger nextIndex = new AtomicInteger();
     private final int maxIndex;
 
@@ -1157,67 +1473,23 @@
      *
      * @param factories
      *          The list of underlying connection factories.
+     * @param scheduler
+     *          The monitoring scheduler.
      */
-    LoadBalancer(final ConnectionFactory[] factories)
+    RoundRobinLoadBalancer(final ConnectionFactory[] factories,
+        final ScheduledExecutorService scheduler)
     {
-      this.factories = factories;
+      super(factories, scheduler);
       this.maxIndex = factories.length;
     }
 
 
 
     /**
-     * Close underlying connection pools.
-     */
-    @Override
-    public void close()
-    {
-      for (final ConnectionFactory factory : factories)
-      {
-        factory.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;
-          }
-        }
-      }
-    }
-
-
-
-    // Determine the start index.
-    private int getStartIndex()
+    int getStartIndex()
     {
       // A round robin pool of one connection factories is unlikely in
       // practice and requires special treatment.
@@ -1512,8 +1784,8 @@
     // Current configuration.
     private LDAPPassThroughAuthenticationPolicyCfg cfg;
 
-    private FailoverConnectionFactory searchFactory = null;
-    private FailoverConnectionFactory bindFactory = null;
+    private ConnectionFactory searchFactory = null;
+    private ConnectionFactory bindFactory = null;
 
 
 
@@ -1642,8 +1914,10 @@
       // authenticated user.
 
       // Create load-balancers for primary servers.
-      final LoadBalancer primarySearchLoadBalancer;
-      final LoadBalancer primaryBindLoadBalancer;
+      final RoundRobinLoadBalancer primarySearchLoadBalancer;
+      final RoundRobinLoadBalancer primaryBindLoadBalancer;
+      final ScheduledExecutorService scheduler = provider
+          .getScheduledExecutorService();
 
       Set<String> servers = cfg.getPrimaryRemoteLDAPServer();
       ConnectionPool[] searchPool = new ConnectionPool[servers.size()];
@@ -1658,18 +1932,16 @@
                 cfg.getMappedSearchBindPassword()));
         bindPool[index++] = new ConnectionPool(factory);
       }
-      primarySearchLoadBalancer = new LoadBalancer(searchPool);
-      primaryBindLoadBalancer = new LoadBalancer(bindPool);
+      primarySearchLoadBalancer = new RoundRobinLoadBalancer(searchPool,
+          scheduler);
+      primaryBindLoadBalancer = new RoundRobinLoadBalancer(bindPool, scheduler);
 
       // Create load-balancers for secondary servers.
-      final LoadBalancer secondarySearchLoadBalancer;
-      final LoadBalancer secondaryBindLoadBalancer;
-
       servers = cfg.getSecondaryRemoteLDAPServer();
       if (servers.isEmpty())
       {
-        secondarySearchLoadBalancer = null;
-        secondaryBindLoadBalancer = null;
+        searchFactory = primarySearchLoadBalancer;
+        bindFactory = primaryBindLoadBalancer;
       }
       else
       {
@@ -1685,14 +1957,15 @@
                   cfg.getMappedSearchBindPassword()));
           bindPool[index++] = new ConnectionPool(factory);
         }
-        secondarySearchLoadBalancer = new LoadBalancer(searchPool);
-        secondaryBindLoadBalancer = new LoadBalancer(bindPool);
+        final RoundRobinLoadBalancer secondarySearchLoadBalancer =
+          new RoundRobinLoadBalancer(searchPool, scheduler);
+        final RoundRobinLoadBalancer secondaryBindLoadBalancer =
+          new RoundRobinLoadBalancer(bindPool, scheduler);
+        searchFactory = new FailoverLoadBalancer(primarySearchLoadBalancer,
+            secondarySearchLoadBalancer, scheduler);
+        bindFactory = new FailoverLoadBalancer(primaryBindLoadBalancer,
+            secondaryBindLoadBalancer, scheduler);
       }
-
-      searchFactory = new FailoverConnectionFactory(primarySearchLoadBalancer,
-          secondarySearchLoadBalancer);
-      bindFactory = new FailoverConnectionFactory(primaryBindLoadBalancer,
-          secondaryBindLoadBalancer);
     }
 
 
@@ -1725,15 +1998,32 @@
   }
 
   // The provider which should be used by policies to create LDAP connections.
-  private final LDAPConnectionFactoryProvider provider;
+  private final Provider provider;
 
   /**
    * The default LDAP connection factory provider.
    */
-  private static final LDAPConnectionFactoryProvider DEFAULT_PROVIDER =
-    new LDAPConnectionFactoryProvider()
+  private static final Provider DEFAULT_PROVIDER = new Provider()
   {
 
+    // Global scheduler used for periodically monitoring connection factories in
+    // order to detect when they are online.
+    private final ScheduledExecutorService scheduler = Executors
+        .newScheduledThreadPool(2, new ThreadFactory()
+        {
+
+          @Override
+          public Thread newThread(final Runnable r)
+          {
+            final Thread t = new DirectoryThread(r,
+                "LDAP PTA connection monitor thread");
+            t.setDaemon(true);
+            return t;
+          }
+        });
+
+
+
     @Override
     public ConnectionFactory getLDAPConnectionFactory(final String host,
         final int port, final LDAPPassThroughAuthenticationPolicyCfg cfg)
@@ -1741,6 +2031,14 @@
       return new LDAPConnectionFactory(host, port, cfg);
     }
 
+
+
+    @Override
+    public ScheduledExecutorService getScheduledExecutorService()
+    {
+      return scheduler;
+    }
+
   };
 
 
@@ -1843,8 +2141,7 @@
    *          The LDAP connection factory provider implementation which LDAP PTA
    *          authentication policies will use.
    */
-  LDAPPassThroughAuthenticationPolicyFactory(
-      final LDAPConnectionFactoryProvider provider)
+  LDAPPassThroughAuthenticationPolicyFactory(final Provider provider)
   {
     this.provider = provider;
   }
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 d43921d..62d697a 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
@@ -36,8 +36,7 @@
 import java.net.ServerSocket;
 import java.net.Socket;
 import java.util.*;
-import java.util.concurrent.CountDownLatch;
-import java.util.concurrent.TimeUnit;
+import java.util.concurrent.*;
 
 import org.opends.messages.Message;
 import org.opends.server.TestCaseUtils;
@@ -385,6 +384,16 @@
     /**
      * {@inheritDoc}
      */
+    public void close()
+    {
+      // Nothing to do.
+    }
+
+
+
+    /**
+     * {@inheritDoc}
+     */
     @Override
     public Connection getConnection() throws DirectoryException
     {
@@ -403,16 +412,6 @@
       }
     }
 
-
-
-    /**
-     * {@inheritDoc}
-     */
-    public void close()
-    {
-      // Nothing to do.
-    }
-
   }
 
 
@@ -666,10 +665,236 @@
 
 
   static final class MockProvider implements
-      LDAPPassThroughAuthenticationPolicyFactory.LDAPConnectionFactoryProvider
+      LDAPPassThroughAuthenticationPolicyFactory.Provider
   {
 
+    private final class MockScheduledFuture implements ScheduledFuture<Void>
+    {
+      private final Runnable runnable;
+
+
+
+      MockScheduledFuture(final Runnable runnable)
+      {
+        this.runnable = runnable;
+      }
+
+
+
+      public boolean cancel(final boolean mayInterruptIfRunning)
+      {
+        monitorRunnables.remove(this);
+        return true;
+      }
+
+
+
+      public int compareTo(final Delayed o)
+      {
+        return 0;
+      }
+
+
+
+      public Void get() throws InterruptedException, ExecutionException
+      {
+        return null;
+      }
+
+
+
+      public Void get(final long timeout, final TimeUnit unit)
+          throws InterruptedException, ExecutionException, TimeoutException
+      {
+        return null;
+      }
+
+
+
+      public long getDelay(final TimeUnit unit)
+      {
+        return 0;
+      }
+
+
+
+      public boolean isCancelled()
+      {
+        return false;
+      }
+
+
+
+      public boolean isDone()
+      {
+        return false;
+      }
+
+
+
+      Runnable getRunnable()
+      {
+        return runnable;
+      }
+    }
+
+
+
     private final Queue<Event<?>> expectedEvents = new LinkedList<Event<?>>();
+    private final List<MockScheduledFuture> monitorRunnables = new LinkedList<MockScheduledFuture>();
+
+    // All methods unused excepted scheduleWithFixedDelay.
+    private final ScheduledExecutorService mockScheduler = new ScheduledExecutorService()
+    {
+
+      @Override
+      public boolean awaitTermination(final long timeout, final TimeUnit unit)
+          throws InterruptedException
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public void execute(final Runnable command)
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public <T> List<Future<T>> invokeAll(
+          final Collection<? extends Callable<T>> tasks)
+          throws InterruptedException
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public <T> List<Future<T>> invokeAll(
+          final Collection<? extends Callable<T>> tasks, final long timeout,
+          final TimeUnit unit) throws InterruptedException
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public <T> T invokeAny(final Collection<? extends Callable<T>> tasks)
+          throws InterruptedException, ExecutionException
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public <T> T invokeAny(final Collection<? extends Callable<T>> tasks,
+          final long timeout, final TimeUnit unit) throws InterruptedException,
+          ExecutionException, TimeoutException
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public boolean isShutdown()
+      {
+        return false;
+      }
+
+
+
+      @Override
+      public boolean isTerminated()
+      {
+        return false;
+      }
+
+
+
+      @Override
+      public <V> ScheduledFuture<V> schedule(final Callable<V> callable,
+          final long delay, final TimeUnit unit)
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public ScheduledFuture<?> schedule(final Runnable command,
+          final long delay, final TimeUnit unit)
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public ScheduledFuture<?> scheduleAtFixedRate(final Runnable command,
+          final long initialDelay, final long period, final TimeUnit unit)
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public ScheduledFuture<?> scheduleWithFixedDelay(final Runnable command,
+          final long initialDelay, final long delay, final TimeUnit unit)
+      {
+        final MockScheduledFuture future = new MockScheduledFuture(command);
+        monitorRunnables.add(future);
+        return future;
+      }
+
+
+
+      @Override
+      public void shutdown()
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public List<Runnable> shutdownNow()
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public <T> Future<T> submit(final Callable<T> task)
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public Future<?> submit(final Runnable task)
+      {
+        throw new UnsupportedOperationException();
+      }
+
+
+
+      @Override
+      public <T> Future<T> submit(final Runnable task, final T result)
+      {
+        throw new UnsupportedOperationException();
+      }
+    };
 
 
 
@@ -688,6 +913,16 @@
 
 
 
+    /**
+     * {@inheritDoc}
+     */
+    public ScheduledExecutorService getScheduledExecutorService()
+    {
+      return mockScheduler;
+    }
+
+
+
     void assertAllExpectedEventsReceived()
     {
       assertTrue(expectedEvents.isEmpty());
@@ -717,6 +952,17 @@
       expectedEvents.add(expectedEvent);
       return this;
     }
+
+
+
+    void runMonitorTasks()
+    {
+      for (final MockScheduledFuture task : monitorRunnables)
+      {
+        task.getRunnable().run();
+      }
+    }
+
   }
 
 
@@ -1517,6 +1763,468 @@
 
 
   /**
+   * Tests fail-over between 2 primary servers then to the secondary data
+   * center.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testFailOverOnConnect() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1).withPrimaryServer(phost2)
+        .withSecondaryServer(shost1)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
+
+    // Create all the events.
+    final MockProvider provider = new MockProvider();
+
+    // First of all the connection factories are created.
+    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
+        phost1, cfg);
+    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
+        phost2, cfg);
+    final GetLDAPConnectionFactoryEvent fe3 = new GetLDAPConnectionFactoryEvent(
+        shost1, cfg);
+    provider.expectEvent(fe1).expectEvent(fe2).expectEvent(fe3);
+
+    // Get connection for phost1, then search, then bind.
+    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1,
+        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2,
+        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+    final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe3);
+
+    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1,
+        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2,
+        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
+    final GetConnectionEvent ceBind3 = new GetConnectionEvent(fe3);
+
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(ceSearch2)
+        .expectEvent(ceSearch3)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch3, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind1)
+        .expectEvent(ceBind2)
+        .expectEvent(ceBind3)
+        .expectEvent(
+            new SimpleBindEvent(ceBind3, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Repeat again using cached connection to shost1: search, then bind.
+
+    // phost1 and phost2 will have been marked as failed and won't be tried.
+    provider.expectEvent(
+        new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", adDNString)).expectEvent(
+        new SimpleBindEvent(ceBind3, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Now simulate monitor thread run in which phost2 is determined to be
+    // available again.
+    final GetConnectionEvent ceSearch2ok = new GetConnectionEvent(fe2);
+    final GetConnectionEvent ceBind2ok = new GetConnectionEvent(fe2);
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(ceSearch2ok)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch2ok, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS)).expectEvent(ceBind1)
+        .expectEvent(ceBind2ok);
+
+    provider.expectEvent(
+        new SearchEvent(ceSearch2ok, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", adDNString)).expectEvent(
+        new SimpleBindEvent(ceBind2ok, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Connections should be cached until the policy is finalized.
+
+    // Obtain policy and state.
+    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
+        provider);
+    assertTrue(factory.isConfigurationAcceptable(cfg, null));
+    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
+
+    // Authenticate 3 times test above fail-over.
+    for (int i = 0; i < 3; i++)
+    {
+      final AuthenticationPolicyState state = policy
+          .createAuthenticationPolicyState(userEntry);
+      assertEquals(state.getAuthenticationPolicy(), policy);
+
+      // Run monitor which should try to connect to phost1&2 and determine that
+      // phost2 is available again.
+      if (i == 2)
+      {
+        provider.runMonitorTasks();
+      }
+
+      // Perform authentication.
+      assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+
+      state.finalizeStateAfterBind();
+    }
+
+    // Cached connections should be closed when the policy is finalized
+    // (primaries first, then secondaries).
+    provider.expectEvent(new CloseEvent(ceSearch2ok));
+    provider.expectEvent(new CloseEvent(ceSearch3));
+    provider.expectEvent(new CloseEvent(ceBind2ok));
+    provider.expectEvent(new CloseEvent(ceBind3));
+
+    // Tear down and check final state.
+    policy.finalizeAuthenticationPolicy();
+    provider.assertAllExpectedEventsReceived();
+  }
+
+
+
+  /**
+   * Tests that searches which fail in one LB pool are automatically retried in
+   * the secondary LB pool.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testFBRetrySearchOnFailure() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1).withSecondaryServer(shost1)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
+
+    // Create all the events.
+    final MockProvider provider = new MockProvider();
+
+    // First of all the connection factories are created.
+    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
+        phost1, cfg);
+    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
+        shost1, cfg);
+    provider.expectEvent(fe1).expectEvent(fe2);
+
+    // Get connection for phost1, then search (fail), and retry on shost1
+    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
+    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
+    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
+
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch1, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", ResultCode.UNAVAILABLE))
+        .expectEvent(new CloseEvent(ceSearch1))
+        .expectEvent(ceSearch2)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch2, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind1)
+        .expectEvent(
+            new SimpleBindEvent(ceBind1, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Now simulate shost1 going down as well.
+
+    // phost1 will have been marked as failed and won't be retried.
+    provider.expectEvent(
+        new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", ResultCode.UNAVAILABLE)).expectEvent(
+        new CloseEvent(ceSearch2));
+
+    // Now simulate phost1 coming back and fail back to it.
+
+    // Now simulate monitor thread run in which phost1 and shost1 are determined
+    // to be available again.
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch1, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(ceSearch2)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch2, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS));
+
+    provider.expectEvent(
+        new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", adDNString)).expectEvent(
+        new SimpleBindEvent(ceBind1, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Connections should be cached until the policy is finalized.
+
+    // Obtain policy and state.
+    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
+        provider);
+    assertTrue(factory.isConfigurationAcceptable(cfg, null));
+    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
+
+    // Authenticate 3 times, second should fail.
+    for (int i = 0; i < 3; i++)
+    {
+      final AuthenticationPolicyState state = policy
+          .createAuthenticationPolicyState(userEntry);
+      assertEquals(state.getAuthenticationPolicy(), policy);
+
+      // Perform authentication.
+      switch (i)
+      {
+      case 0:
+        // First attempt should succeed.
+        assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+        break;
+      case 1:
+        // Second attempt should fail.
+        try
+        {
+          state.passwordMatches(ByteString.valueOf(userPassword));
+          fail("password match unexpectedly succeeded");
+        }
+        catch (final DirectoryException e)
+        {
+          // No valid connections available so this should always fail with
+          // INVALID_CREDENTIALS.
+          assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+              e.getMessage());
+        }
+        break;
+      case 2:
+        // Third attempt should succeed, once the monitor has run.
+        provider.runMonitorTasks();
+        assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+        break;
+      }
+
+      state.finalizeStateAfterBind();
+    }
+
+    // Cached connections should be closed when the policy is finalized.
+    provider.expectEvent(new CloseEvent(ceSearch1));
+    provider.expectEvent(new CloseEvent(ceSearch2));
+    provider.expectEvent(new CloseEvent(ceBind1));
+
+    // Tear down and check final state.
+    policy.finalizeAuthenticationPolicy();
+    provider.assertAllExpectedEventsReceived();
+  }
+
+
+
+  /**
+   * Tests configuration validation.
+   *
+   * @param cfg
+   *          The configuration to be tested.
+   * @param isValid
+   *          Whether or not the provided configuration is valid.
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true, dataProvider = "testIsConfigurationAcceptableData")
+  public void testIsConfigurationAcceptable(
+      final LDAPPassThroughAuthenticationPolicyCfg cfg, final boolean isValid)
+      throws Exception
+  {
+    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory();
+    assertEquals(
+        factory.isConfigurationAcceptable(cfg, new LinkedList<Message>()),
+        isValid);
+  }
+
+
+
+  /**
+   * Returns test data for {@link #testIsConfigurationAcceptable}.
+   *
+   * @return Test data for {@link #testIsConfigurationAcceptable}.
+   */
+  @DataProvider
+  public Object[][] testIsConfigurationAcceptableData()
+  {
+    // @formatter:off
+    return new Object[][] {
+        /* cfg, isValid */
+        { mockCfg().withPrimaryServer("test:1"), true },
+        { mockCfg().withPrimaryServer("test:65535"), true },
+        { mockCfg().withPrimaryServer("test:0"), false },
+        { mockCfg().withPrimaryServer("test:65536"), false },
+        { mockCfg().withPrimaryServer("test:1000000"), false },
+        { mockCfg().withSecondaryServer("test:1"), true },
+        { mockCfg().withSecondaryServer("test:65535"), true },
+        { mockCfg().withSecondaryServer("test:0"), false },
+        { mockCfg().withSecondaryServer("test:65536"), false },
+        { mockCfg().withSecondaryServer("test:1000000"), false },
+    };
+    // @formatter:on
+  }
+
+
+
+  /**
+   * Tests that searches which fail on one server are automatically retried on
+   * another within the same LB.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLBRetrySearchOnFailure() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1).withPrimaryServer(phost2)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
+
+    // Create all the events.
+    final MockProvider provider = new MockProvider();
+
+    // First of all the connection factories are created.
+    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
+        phost1, cfg);
+    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
+        phost2, cfg);
+    provider.expectEvent(fe1).expectEvent(fe2);
+
+    // Get connection for phost1, then search (fail), and retry on phost2
+    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
+    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
+    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
+    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
+
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch1, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", ResultCode.UNAVAILABLE))
+        .expectEvent(new CloseEvent(ceSearch1))
+        .expectEvent(ceSearch2)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch2, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind1)
+        .expectEvent(
+            new SimpleBindEvent(ceBind1, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Now simulate phost2 going down as well.
+
+    // phost1 will have been marked as failed and won't be retried.
+    provider.expectEvent(
+        new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", ResultCode.UNAVAILABLE)).expectEvent(
+        new CloseEvent(ceSearch2));
+
+    // Now simulate phost1 coming back and fail back to it.
+
+    // Now simulate monitor thread run in which phost1 and shost1 are determined
+    // to be available again.
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch1, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(ceSearch2)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch2, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS));
+
+    // Note that the bind will be load-balanced.
+    provider
+        .expectEvent(
+            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind2)
+        .expectEvent(
+            new SimpleBindEvent(ceBind2, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Connections should be cached until the policy is finalized.
+
+    // Obtain policy and state.
+    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
+        provider);
+    assertTrue(factory.isConfigurationAcceptable(cfg, null));
+    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
+
+    // Authenticate 3 times, second should fail.
+    for (int i = 0; i < 3; i++)
+    {
+      final AuthenticationPolicyState state = policy
+          .createAuthenticationPolicyState(userEntry);
+      assertEquals(state.getAuthenticationPolicy(), policy);
+
+      // Perform authentication.
+      switch (i)
+      {
+      case 0:
+        // First attempt should succeed.
+        assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+        break;
+      case 1:
+        // Second attempt should fail.
+        try
+        {
+          state.passwordMatches(ByteString.valueOf(userPassword));
+          fail("password match unexpectedly succeeded");
+        }
+        catch (final DirectoryException e)
+        {
+          // No valid connections available so this should always fail with
+          // INVALID_CREDENTIALS.
+          assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+              e.getMessage());
+        }
+        break;
+      case 2:
+        // Third attempt should succeed, once the monitor has run.
+        provider.runMonitorTasks();
+        assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+        break;
+      }
+
+      state.finalizeStateAfterBind();
+    }
+
+    // Cached connections should be closed when the policy is finalized.
+    provider.expectEvent(new CloseEvent(ceSearch1));
+    provider.expectEvent(new CloseEvent(ceSearch2));
+    provider.expectEvent(new CloseEvent(ceBind1));
+    provider.expectEvent(new CloseEvent(ceBind2));
+
+    // Tear down and check final state.
+    policy.finalizeAuthenticationPolicy();
+    provider.assertAllExpectedEventsReceived();
+  }
+
+
+
+  /**
    * Tests valid bind which times out at the client. These should trigger a
    * CLIENT_SIDE_TIMEOUT result code.
    *
@@ -2339,6 +3047,139 @@
 
 
   /**
+   * Tests load balancing across 3 servers.
+   *
+   * @throws Exception
+   *           If an unexpected exception occurred.
+   */
+  @Test(enabled = true)
+  public void testLoadBalancing() throws Exception
+  {
+    // Mock configuration.
+    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
+        .withPrimaryServer(phost1).withPrimaryServer(phost2)
+        .withPrimaryServer(phost3)
+        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
+        .withMappedAttribute("uid").withBaseDN("o=ad");
+
+    // Create all the events.
+    final MockProvider provider = new MockProvider();
+
+    // First of all the connection factories are created.
+    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
+        phost1, cfg);
+    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
+        phost2, cfg);
+    final GetLDAPConnectionFactoryEvent fe3 = new GetLDAPConnectionFactoryEvent(
+        phost3, cfg);
+    provider.expectEvent(fe1).expectEvent(fe2).expectEvent(fe3);
+
+    // Get connection for phost1, then search, then bind.
+    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
+    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
+    provider
+        .expectEvent(ceSearch1)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch1, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind1)
+        .expectEvent(
+            new SimpleBindEvent(ceBind1, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Get connection for phost2, then search, then bind.
+    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
+    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
+    provider
+        .expectEvent(ceSearch2)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch2, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind2)
+        .expectEvent(
+            new SimpleBindEvent(ceBind2, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Get connection for phost3, then search, then bind.
+    final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe3);
+    final GetConnectionEvent ceBind3 = new GetConnectionEvent(fe3);
+    provider
+        .expectEvent(ceSearch3)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch3, searchBindDNString,
+                "searchPassword", ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString))
+        .expectEvent(ceBind3)
+        .expectEvent(
+            new SimpleBindEvent(ceBind3, adDNString, userPassword,
+                ResultCode.SUCCESS));
+
+    // Repeat again using cached connection to phost1: search, then bind.
+    provider.expectEvent(
+        new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", adDNString)).expectEvent(
+        new SimpleBindEvent(ceBind1, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Repeat again using cached connection to phost2: search, then bind.
+    provider.expectEvent(
+        new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", adDNString)).expectEvent(
+        new SimpleBindEvent(ceBind2, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Repeat again using cached connection to phost3: search, then bind.
+    provider.expectEvent(
+        new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
+            "(uid=aduser)", adDNString)).expectEvent(
+        new SimpleBindEvent(ceBind3, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Connections should be cached until the policy is finalized.
+
+    // Obtain policy and state.
+    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
+        provider);
+    assertTrue(factory.isConfigurationAcceptable(cfg, null));
+    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
+
+    // Cycle twice through the LB pool.
+    for (int i = 0; i < 6; i++)
+    {
+      final AuthenticationPolicyState state = policy
+          .createAuthenticationPolicyState(userEntry);
+      assertEquals(state.getAuthenticationPolicy(), policy);
+
+      // Perform authentication.
+      assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+
+      state.finalizeStateAfterBind();
+    }
+
+    // Cached connections should be closed when the policy is finalized.
+    provider.expectEvent(new CloseEvent(ceSearch1));
+    provider.expectEvent(new CloseEvent(ceSearch2));
+    provider.expectEvent(new CloseEvent(ceSearch3));
+    provider.expectEvent(new CloseEvent(ceBind1));
+    provider.expectEvent(new CloseEvent(ceBind2));
+    provider.expectEvent(new CloseEvent(ceBind3));
+
+    // Tear down and check final state.
+    policy.finalizeAuthenticationPolicy();
+    provider.assertAllExpectedEventsReceived();
+  }
+
+
+
+  /**
    * 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.
@@ -2486,123 +3327,63 @@
 
 
   /**
-   * Tests configuration validation.
-   *
-   * @param cfg
-   *          The configuration to be tested.
-   * @param isValid
-   *          Whether or not the provided configuration is valid.
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = true, dataProvider = "testIsConfigurationAcceptableData")
-  public void testIsConfigurationAcceptable(
-      final LDAPPassThroughAuthenticationPolicyCfg cfg, final boolean isValid)
-      throws Exception
-  {
-    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory();
-    assertEquals(
-        factory.isConfigurationAcceptable(cfg, new LinkedList<Message>()),
-        isValid);
-  }
-
-
-
-  /**
-   * Returns test data for {@link #testIsConfigurationAcceptable}.
-   *
-   * @return Test data for {@link #testIsConfigurationAcceptable}.
-   */
-  @DataProvider
-  public Object[][] testIsConfigurationAcceptableData()
-  {
-    // @formatter:off
-    return new Object[][] {
-        /* cfg, isValid */
-        { mockCfg().withPrimaryServer("test:1"), true },
-        { mockCfg().withPrimaryServer("test:65535"), true },
-        { mockCfg().withPrimaryServer("test:0"), false },
-        { mockCfg().withPrimaryServer("test:65536"), false },
-        { mockCfg().withPrimaryServer("test:1000000"), false },
-        { mockCfg().withSecondaryServer("test:1"), true },
-        { mockCfg().withSecondaryServer("test:65535"), true },
-        { mockCfg().withSecondaryServer("test:0"), false },
-        { mockCfg().withSecondaryServer("test:65536"), false },
-        { mockCfg().withSecondaryServer("test:1000000"), false },
-    };
-    // @formatter:on
-  }
-
-
-
-  /**
-   * Tests that mapped PTA performs searches across multiple base DNs if
-   * configured.
+   * Tests that mapped PTA fails when no match attribute values are found.
    *
    * @throws Exception
    *           If an unexpected exception occurred.
    */
   @Test(enabled = true)
-  public void testMultipleSearchBaseDNs() throws Exception
+  public void testMissingMappingAttributes() throws Exception
   {
     // Mock configuration.
     final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
         .withPrimaryServer(phost1)
         .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
-        .withMappedAttribute("uid").withBaseDN("o=first")
-        .withBaseDN("o=second").withBaseDN("o=third");
+        .withMappedAttribute("uid1").withBaseDN("o=ad");
 
     // Create the provider and its list of expected events.
     final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
         phost1, cfg);
     final MockProvider provider = new MockProvider().expectEvent(fe);
 
-    // Add search events.
-    GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
-    provider
-        .expectEvent(ceSearch)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
-                ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch, "o=first", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED))
-        .expectEvent(
-            new SearchEvent(ceSearch, "o=second", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED))
-        .expectEvent(
-            new SearchEvent(ceSearch, "o=third", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString));
-    // Connection should be cached until the policy is finalized.
-
-    // Add bind events.
-    final GetConnectionEvent ceBind = new GetConnectionEvent(fe);
-    provider.expectEvent(ceBind).expectEvent(
-        new SimpleBindEvent(ceBind, adDNString, userPassword,
-            ResultCode.SUCCESS));
-
-    // Connection should be cached until the policy is finalized.
-
     // Obtain policy and state.
+    final Entry testUser = TestCaseUtils.makeEntry(
+        /* @formatter:off */
+        "dn: " + opendjDNString,
+        "objectClass: top",
+        "objectClass: person",
+        "sn: user",
+        "cn: test user",
+        "aduser: " + adDNString
+        /* @formatter:on */
+    );
+
     final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
         provider);
     assertTrue(factory.isConfigurationAcceptable(cfg, null));
     final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
     final AuthenticationPolicyState state = policy
-        .createAuthenticationPolicyState(userEntry);
+        .createAuthenticationPolicyState(testUser);
     assertEquals(state.getAuthenticationPolicy(), policy);
 
     // Perform authentication.
-    assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
+    try
+    {
+      state.passwordMatches(ByteString.valueOf(userPassword));
+      fail("password match unexpectedly succeeded");
+    }
+    catch (final DirectoryException e)
+    {
+      // No mapping attributes so this should always fail with
+      // INVALID_CREDENTIALS.
+      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
+          e.getMessage());
+    }
 
     // There should be no more pending events.
     provider.assertAllExpectedEventsReceived();
     state.finalizeStateAfterBind();
 
-    // Cached connections should be closed when the policy is finalized.
-    provider.expectEvent(new CloseEvent(ceSearch));
-    provider.expectEvent(new CloseEvent(ceBind));
-
     // Tear down and check final state.
     policy.finalizeAuthenticationPolicy();
     provider.assertAllExpectedEventsReceived();
@@ -2633,7 +3414,7 @@
     final MockProvider provider = new MockProvider().expectEvent(fe);
 
     // Add search events.
-    GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
+    final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
     provider
         .expectEvent(ceSearch)
         .expectEvent(
@@ -2653,7 +3434,7 @@
     // Connection should be cached until the policy is finalized.
 
     // Obtain policy and state.
-    Entry testUser = TestCaseUtils.makeEntry(
+    final Entry testUser = TestCaseUtils.makeEntry(
         /* @formatter:off */
         "dn: " + opendjDNString,
         "objectClass: top",
@@ -2715,7 +3496,7 @@
     final MockProvider provider = new MockProvider().expectEvent(fe);
 
     // Add search events.
-    GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
+    final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
     provider
         .expectEvent(ceSearch)
         .expectEvent(
@@ -2735,7 +3516,7 @@
     // Connection should be cached until the policy is finalized.
 
     // Obtain policy and state.
-    Entry testUser = TestCaseUtils.makeEntry(
+    final Entry testUser = TestCaseUtils.makeEntry(
         /* @formatter:off */
         "dn: " + opendjDNString,
         "objectClass: top",
@@ -2776,594 +3557,72 @@
 
 
   /**
-   * Tests that mapped PTA fails when no match attribute values are found.
+   * Tests that mapped PTA performs searches across multiple base DNs if
+   * configured.
    *
    * @throws Exception
    *           If an unexpected exception occurred.
    */
   @Test(enabled = true)
-  public void testMissingMappingAttributes() throws Exception
+  public void testMultipleSearchBaseDNs() throws Exception
   {
     // Mock configuration.
     final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
         .withPrimaryServer(phost1)
         .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
-        .withMappedAttribute("uid1").withBaseDN("o=ad");
+        .withMappedAttribute("uid").withBaseDN("o=first")
+        .withBaseDN("o=second").withBaseDN("o=third");
 
     // Create the provider and its list of expected events.
     final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
         phost1, cfg);
     final MockProvider provider = new MockProvider().expectEvent(fe);
 
-    // Obtain policy and state.
-    Entry testUser = TestCaseUtils.makeEntry(
-        /* @formatter:off */
-        "dn: " + opendjDNString,
-        "objectClass: top",
-        "objectClass: person",
-        "sn: user",
-        "cn: test user",
-        "aduser: " + adDNString
-        /* @formatter:on */
-    );
+    // Add search events.
+    final GetConnectionEvent ceSearch = new GetConnectionEvent(fe);
+    provider
+        .expectEvent(ceSearch)
+        .expectEvent(
+            new SimpleBindEvent(ceSearch, searchBindDNString, "searchPassword",
+                ResultCode.SUCCESS))
+        .expectEvent(
+            new SearchEvent(ceSearch, "o=first", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED))
+        .expectEvent(
+            new SearchEvent(ceSearch, "o=second", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED))
+        .expectEvent(
+            new SearchEvent(ceSearch, "o=third", SearchScope.WHOLE_SUBTREE,
+                "(uid=aduser)", adDNString));
+    // Connection should be cached until the policy is finalized.
 
+    // Add bind events.
+    final GetConnectionEvent ceBind = new GetConnectionEvent(fe);
+    provider.expectEvent(ceBind).expectEvent(
+        new SimpleBindEvent(ceBind, adDNString, userPassword,
+            ResultCode.SUCCESS));
+
+    // Connection should be cached until the policy is finalized.
+
+    // Obtain policy and state.
     final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
         provider);
     assertTrue(factory.isConfigurationAcceptable(cfg, null));
     final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
     final AuthenticationPolicyState state = policy
-        .createAuthenticationPolicyState(testUser);
+        .createAuthenticationPolicyState(userEntry);
     assertEquals(state.getAuthenticationPolicy(), policy);
 
     // Perform authentication.
-    try
-    {
-      state.passwordMatches(ByteString.valueOf(userPassword));
-      fail("password match unexpectedly succeeded");
-    }
-    catch (final DirectoryException e)
-    {
-      // No mapping attributes so this should always fail with
-      // INVALID_CREDENTIALS.
-      assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
-          e.getMessage());
-    }
+    assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
 
     // There should be no more pending events.
     provider.assertAllExpectedEventsReceived();
     state.finalizeStateAfterBind();
 
-    // Tear down and check final state.
-    policy.finalizeAuthenticationPolicy();
-    provider.assertAllExpectedEventsReceived();
-  }
-
-
-
-  /**
-   * Tests load balancing across 3 servers.
-   *
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = true)
-  public void testLoadBalancing() throws Exception
-  {
-    // Mock configuration.
-    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
-        .withPrimaryServer(phost1).withPrimaryServer(phost2)
-        .withPrimaryServer(phost3)
-        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
-        .withMappedAttribute("uid").withBaseDN("o=ad");
-
-    // Create all the events.
-    final MockProvider provider = new MockProvider();
-
-    // First of all the connection factories are created.
-    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
-        phost1, cfg);
-    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
-        phost2, cfg);
-    final GetLDAPConnectionFactoryEvent fe3 = new GetLDAPConnectionFactoryEvent(
-        phost3, cfg);
-    provider.expectEvent(fe1).expectEvent(fe2).expectEvent(fe3);
-
-    // Get connection for phost1, then search, then bind.
-    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
-    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind1)
-        .expectEvent(
-            new SimpleBindEvent(ceBind1, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Get connection for phost2, then search, then bind.
-    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
-    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
-    provider
-        .expectEvent(ceSearch2)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch2, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind2)
-        .expectEvent(
-            new SimpleBindEvent(ceBind2, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Get connection for phost3, then search, then bind.
-    final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe3);
-    final GetConnectionEvent ceBind3 = new GetConnectionEvent(fe3);
-    provider
-        .expectEvent(ceSearch3)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch3, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind3)
-        .expectEvent(
-            new SimpleBindEvent(ceBind3, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Repeat again using cached connection to phost1: search, then bind.
-    provider.expectEvent(
-        new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-            "(uid=aduser)", adDNString)).expectEvent(
-        new SimpleBindEvent(ceBind1, adDNString, userPassword,
-            ResultCode.SUCCESS));
-
-    // Repeat again using cached connection to phost2: search, then bind.
-    provider.expectEvent(
-        new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
-            "(uid=aduser)", adDNString)).expectEvent(
-        new SimpleBindEvent(ceBind2, adDNString, userPassword,
-            ResultCode.SUCCESS));
-
-    // Repeat again using cached connection to phost3: search, then bind.
-    provider.expectEvent(
-        new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
-            "(uid=aduser)", adDNString)).expectEvent(
-        new SimpleBindEvent(ceBind3, adDNString, userPassword,
-            ResultCode.SUCCESS));
-
-    // Connections should be cached until the policy is finalized.
-
-    // Obtain policy and state.
-    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
-        provider);
-    assertTrue(factory.isConfigurationAcceptable(cfg, null));
-    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
-
-    // Cycle twice through the LB pool.
-    for (int i = 0; i < 6; i++)
-    {
-      final AuthenticationPolicyState state = policy
-          .createAuthenticationPolicyState(userEntry);
-      assertEquals(state.getAuthenticationPolicy(), policy);
-
-      // Perform authentication.
-      assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
-
-      state.finalizeStateAfterBind();
-    }
-
     // Cached connections should be closed when the policy is finalized.
-    provider.expectEvent(new CloseEvent(ceSearch1));
-    provider.expectEvent(new CloseEvent(ceSearch2));
-    provider.expectEvent(new CloseEvent(ceSearch3));
-    provider.expectEvent(new CloseEvent(ceBind1));
-    provider.expectEvent(new CloseEvent(ceBind2));
-    provider.expectEvent(new CloseEvent(ceBind3));
-
-    // Tear down and check final state.
-    policy.finalizeAuthenticationPolicy();
-    provider.assertAllExpectedEventsReceived();
-  }
-
-
-
-  /**
-   * Tests fail-over between 2 primary servers then to the secondary data
-   * center.
-   *
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = true)
-  public void testFailOverOnConnect() throws Exception
-  {
-    // Mock configuration.
-    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
-        .withPrimaryServer(phost1).withPrimaryServer(phost2)
-        .withSecondaryServer(shost1)
-        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
-        .withMappedAttribute("uid").withBaseDN("o=ad");
-
-    // Create all the events.
-    final MockProvider provider = new MockProvider();
-
-    // First of all the connection factories are created.
-    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
-        phost1, cfg);
-    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
-        phost2, cfg);
-    final GetLDAPConnectionFactoryEvent fe3 = new GetLDAPConnectionFactoryEvent(
-        shost1, cfg);
-    provider.expectEvent(fe1).expectEvent(fe2).expectEvent(fe3);
-
-    // Get connection for phost1, then search, then bind.
-    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1,
-        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
-    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2,
-        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
-    final GetConnectionEvent ceSearch3 = new GetConnectionEvent(fe3);
-
-    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1,
-        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
-    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2,
-        ResultCode.CLIENT_SIDE_CONNECT_ERROR);
-    final GetConnectionEvent ceBind3 = new GetConnectionEvent(fe3);
-
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(ceSearch2)
-        .expectEvent(ceSearch3)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch3, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind1)
-        .expectEvent(ceBind2)
-        .expectEvent(ceBind3)
-        .expectEvent(
-            new SimpleBindEvent(ceBind3, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Repeat again using cached connection to shost1: search, then bind.
-
-    // Note that LB will cause phost2 to be tried first, hence ceSearch2 then
-    // ceSearch1.
-    provider
-        .expectEvent(ceSearch2)
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SearchEvent(ceSearch3, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind2)
-        .expectEvent(ceBind1)
-        .expectEvent(
-            new SimpleBindEvent(ceBind3, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Now simulate phost2 coming back and fail back to it
-    final GetConnectionEvent ceSearch2ok = new GetConnectionEvent(fe2);
-    final GetConnectionEvent ceBind2ok = new GetConnectionEvent(fe2);
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(ceSearch2ok)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch2ok, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch2ok, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind1)
-        .expectEvent(ceBind2ok)
-        .expectEvent(
-            new SimpleBindEvent(ceBind2ok, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Connections should be cached until the policy is finalized.
-
-    // Obtain policy and state.
-    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
-        provider);
-    assertTrue(factory.isConfigurationAcceptable(cfg, null));
-    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
-
-    // Authenticate 3 times test above fail-over.
-    for (int i = 0; i < 3; i++)
-    {
-      final AuthenticationPolicyState state = policy
-          .createAuthenticationPolicyState(userEntry);
-      assertEquals(state.getAuthenticationPolicy(), policy);
-
-      // Perform authentication.
-      assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
-
-      state.finalizeStateAfterBind();
-    }
-
-    // Cached connections should be closed when the policy is finalized
-    // (primaries first, then secondaries).
-    provider.expectEvent(new CloseEvent(ceSearch2ok));
-    provider.expectEvent(new CloseEvent(ceSearch3));
-    provider.expectEvent(new CloseEvent(ceBind2ok));
-    provider.expectEvent(new CloseEvent(ceBind3));
-
-    // Tear down and check final state.
-    policy.finalizeAuthenticationPolicy();
-    provider.assertAllExpectedEventsReceived();
-  }
-
-
-
-  /**
-   * Tests that searches which fail on one server are automatically retried on
-   * another within the same LB.
-   *
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = false)
-  public void testLBRetrySearchOnFailure() throws Exception
-  {
-    // Mock configuration.
-    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
-        .withPrimaryServer(phost1).withPrimaryServer(phost2)
-        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
-        .withMappedAttribute("uid").withBaseDN("o=ad");
-
-    // Create all the events.
-    final MockProvider provider = new MockProvider();
-
-    // First of all the connection factories are created.
-    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
-        phost1, cfg);
-    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
-        phost2, cfg);
-    provider.expectEvent(fe1).expectEvent(fe2);
-
-    // Get connection for phost1, then search (fail), and retry on phost2
-    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
-    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
-
-    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
-    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
-
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.UNAVAILABLE))
-        .expectEvent(new CloseEvent(ceSearch1))
-        .expectEvent(ceSearch2)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch2, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind1)
-        .expectEvent(
-            new SimpleBindEvent(ceBind1, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Now simulate phost2 going down as well.
-
-    // Note that LB will cause phost2 to be tried first, hence ceSearch2 then
-    // ceSearch1.
-    provider
-        .expectEvent(ceSearch2)
-        .expectEvent(
-            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.UNAVAILABLE))
-        .expectEvent(new CloseEvent(ceSearch2))
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.UNAVAILABLE))
-        .expectEvent(new CloseEvent(ceSearch1));
-
-    // Now simulate phost1 coming back and fail back to it.
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString));
-
-    // Connections should be cached until the policy is finalized.
-
-    // Obtain policy and state.
-    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
-        provider);
-    assertTrue(factory.isConfigurationAcceptable(cfg, null));
-    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
-
-    // Authenticate 3 times, second should fail.
-    for (int i = 0; i < 3; i++)
-    {
-      final AuthenticationPolicyState state = policy
-          .createAuthenticationPolicyState(userEntry);
-      assertEquals(state.getAuthenticationPolicy(), policy);
-
-      // Perform authentication.
-      if (i != 1)
-      {
-        // First and third attempt should succeed.
-        assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
-      }
-      else
-      {
-        // Second attempt should fail.
-        try
-        {
-          state.passwordMatches(ByteString.valueOf(userPassword));
-          fail("password match unexpectedly succeeded");
-        }
-        catch (final DirectoryException e)
-        {
-          // No valid connections available so this should always fail with
-          // INVALID_CREDENTIALS.
-          assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
-              e.getMessage());
-        }
-      }
-
-      state.finalizeStateAfterBind();
-    }
-
-    // Cached connections should be closed when the policy is finalized.
-    provider.expectEvent(new CloseEvent(ceSearch1));
-    provider.expectEvent(new CloseEvent(ceBind1));
-    provider.expectEvent(new CloseEvent(ceBind2));
-
-    // Tear down and check final state.
-    policy.finalizeAuthenticationPolicy();
-    provider.assertAllExpectedEventsReceived();
-  }
-
-
-
-  /**
-   * Tests that searches which fail in one LB pool are automatically retried in
-   * the secondary LB pool.
-   *
-   * @throws Exception
-   *           If an unexpected exception occurred.
-   */
-  @Test(enabled = false)
-  public void testFBRetrySearchOnFailure() throws Exception
-  {
-    // Mock configuration.
-    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
-        .withPrimaryServer(phost1).withSecondaryServer(shost1)
-        .withMappingPolicy(MappingPolicy.MAPPED_SEARCH)
-        .withMappedAttribute("uid").withBaseDN("o=ad");
-
-    // Create all the events.
-    final MockProvider provider = new MockProvider();
-
-    // First of all the connection factories are created.
-    final GetLDAPConnectionFactoryEvent fe1 = new GetLDAPConnectionFactoryEvent(
-        phost1, cfg);
-    final GetLDAPConnectionFactoryEvent fe2 = new GetLDAPConnectionFactoryEvent(
-        shost1, cfg);
-    provider.expectEvent(fe1).expectEvent(fe2);
-
-    // Get connection for phost1, then search (fail), and retry on shost1
-    final GetConnectionEvent ceSearch1 = new GetConnectionEvent(fe1);
-    final GetConnectionEvent ceSearch2 = new GetConnectionEvent(fe2);
-
-    final GetConnectionEvent ceBind1 = new GetConnectionEvent(fe1);
-    final GetConnectionEvent ceBind2 = new GetConnectionEvent(fe2);
-
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.UNAVAILABLE))
-        .expectEvent(new CloseEvent(ceSearch1))
-        .expectEvent(ceSearch2)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch2, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString))
-        .expectEvent(ceBind1)
-        .expectEvent(
-            new SimpleBindEvent(ceBind1, adDNString, userPassword,
-                ResultCode.SUCCESS));
-
-    // Now simulate shost1 going down as well.
-
-    // Note that FO will retry phost1 again (unlike LB case).
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.UNAVAILABLE))
-        .expectEvent(new CloseEvent(ceSearch1))
-        .expectEvent(ceSearch2)
-        .expectEvent(
-            new SearchEvent(ceSearch2, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", ResultCode.UNAVAILABLE))
-        .expectEvent(new CloseEvent(ceSearch2));
-
-    // Now simulate phost1 coming back and fail back to it.
-    provider
-        .expectEvent(ceSearch1)
-        .expectEvent(
-            new SimpleBindEvent(ceSearch1, searchBindDNString,
-                "searchPassword", ResultCode.SUCCESS))
-        .expectEvent(
-            new SearchEvent(ceSearch1, "o=ad", SearchScope.WHOLE_SUBTREE,
-                "(uid=aduser)", adDNString));
-
-    // Connections should be cached until the policy is finalized.
-
-    // Obtain policy and state.
-    final LDAPPassThroughAuthenticationPolicyFactory factory = new LDAPPassThroughAuthenticationPolicyFactory(
-        provider);
-    assertTrue(factory.isConfigurationAcceptable(cfg, null));
-    final AuthenticationPolicy policy = factory.createAuthenticationPolicy(cfg);
-
-    // Authenticate 3 times, second should fail.
-    for (int i = 0; i < 3; i++)
-    {
-      final AuthenticationPolicyState state = policy
-          .createAuthenticationPolicyState(userEntry);
-      assertEquals(state.getAuthenticationPolicy(), policy);
-
-      // Perform authentication.
-      if (i != 1)
-      {
-        // First and third attempt should succeed.
-        assertTrue(state.passwordMatches(ByteString.valueOf(userPassword)));
-      }
-      else
-      {
-        // Second attempt should fail.
-        try
-        {
-          state.passwordMatches(ByteString.valueOf(userPassword));
-          fail("password match unexpectedly succeeded");
-        }
-        catch (final DirectoryException e)
-        {
-          // No valid connections available so this should always fail with
-          // INVALID_CREDENTIALS.
-          assertEquals(e.getResultCode(), ResultCode.INVALID_CREDENTIALS,
-              e.getMessage());
-        }
-      }
-
-      state.finalizeStateAfterBind();
-    }
-
-    // Cached connections should be closed when the policy is finalized.
-    provider.expectEvent(new CloseEvent(ceSearch1));
-    provider.expectEvent(new CloseEvent(ceBind1));
-    provider.expectEvent(new CloseEvent(ceBind2));
+    provider.expectEvent(new CloseEvent(ceSearch));
+    provider.expectEvent(new CloseEvent(ceBind));
 
     // Tear down and check final state.
     policy.finalizeAuthenticationPolicy();

--
Gitblit v1.10.0