| | |
| | | * |
| | | * |
| | | * Copyright 2010 Sun Microsystems, Inc. |
| | | * Portions copyright 2011 ForgeRock AS |
| | | * Portions copyright 2011-2012 ForgeRock AS |
| | | */ |
| | | |
| | | package org.forgerock.opendj.ldap; |
| | | |
| | | import static org.fest.assertions.Assertions.assertThat; |
| | | import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult; |
| | | import static org.mockito.Matchers.any; |
| | | import static org.mockito.Matchers.anyInt; |
| | | import static org.mockito.Mockito.doAnswer; |
| | | import static org.mockito.Mockito.mock; |
| | | import static org.mockito.Mockito.when; |
| | |
| | | |
| | | import java.net.InetSocketAddress; |
| | | import java.util.Arrays; |
| | | import java.util.concurrent.Callable; |
| | | import java.util.concurrent.CountDownLatch; |
| | | import java.util.concurrent.TimeUnit; |
| | | import java.util.concurrent.TimeoutException; |
| | | import java.util.concurrent.atomic.AtomicInteger; |
| | | import java.util.concurrent.atomic.AtomicReference; |
| | | import java.util.logging.Level; |
| | | |
| | | import javax.net.ssl.SSLContext; |
| | | |
| | | import org.forgerock.opendj.ldap.requests.BindRequest; |
| | | import org.forgerock.opendj.ldap.requests.DigestMD5SASLBindRequest; |
| | | import org.forgerock.opendj.ldap.requests.Requests; |
| | | import org.forgerock.opendj.ldap.requests.SearchRequest; |
| | | import org.forgerock.opendj.ldap.responses.BindResult; |
| | | import org.forgerock.opendj.ldap.responses.Responses; |
| | | import org.forgerock.opendj.ldap.responses.SearchResultEntry; |
| | | import org.forgerock.opendj.ldap.schema.Schema; |
| | | import org.forgerock.opendj.ldap.schema.SchemaBuilder; |
| | |
| | | */ |
| | | @SuppressWarnings("javadoc") |
| | | public class ConnectionFactoryTestCase extends SdkTestCase { |
| | | // Test timeout in ms for tests which need to wait for network events. |
| | | private static final long TEST_TIMEOUT = 30L; |
| | | private static final long TEST_TIMEOUT_MS = TEST_TIMEOUT * 1000L; |
| | | |
| | | class MyResultHandler implements ResultHandler<Connection> { |
| | | // latch. |
| | | private final CountDownLatch latch; |
| | |
| | | StaticUtils.DEBUG_LOG.setLevel(Level.INFO); |
| | | } |
| | | |
| | | @DataProvider(name = "connectionFactories") |
| | | public Object[][] getConnectionFactories() throws Exception { |
| | | @DataProvider |
| | | Object[][] connectionFactories() throws Exception { |
| | | Object[][] factories = new Object[21][1]; |
| | | |
| | | // HeartBeatConnectionFactory |
| | |
| | | assertThat(realConnectionIsClosed[2]).isTrue(); |
| | | assertThat(realConnectionIsClosed[3]).isTrue(); |
| | | } |
| | | |
| | | private static final class CloseNotify { |
| | | private boolean closeOnAccept; |
| | | private boolean doBindFirst; |
| | | private boolean useEventListener; |
| | | private boolean sendDisconnectNotification; |
| | | |
| | | private CloseNotify(boolean closeOnAccept, boolean doBindFirst, boolean useEventListener, |
| | | boolean sendDisconnectNotification) { |
| | | this.closeOnAccept = closeOnAccept; |
| | | this.doBindFirst = doBindFirst; |
| | | this.useEventListener = useEventListener; |
| | | this.sendDisconnectNotification = sendDisconnectNotification; |
| | | } |
| | | |
| | | @Override |
| | | public String toString() { |
| | | StringBuilder builder = new StringBuilder(); |
| | | builder.append("["); |
| | | if (closeOnAccept) { |
| | | builder.append(" closeOnAccept"); |
| | | } |
| | | if (doBindFirst) { |
| | | builder.append(" doBindFirst"); |
| | | } |
| | | if (useEventListener) { |
| | | builder.append(" useEventListener"); |
| | | } |
| | | if (sendDisconnectNotification) { |
| | | builder.append(" sendDisconnectNotification"); |
| | | } |
| | | builder.append(" ]"); |
| | | return builder.toString(); |
| | | } |
| | | } |
| | | |
| | | @DataProvider |
| | | Object[][] closeNotifyConfig() { |
| | | // @formatter:off |
| | | return new Object[][] { |
| | | // closeOnAccept, doBindFirst, useEventListener, sendDisconnectNotification |
| | | |
| | | // Close on accept. |
| | | { new CloseNotify(true, false, false, false) }, |
| | | { new CloseNotify(true, false, true, false) }, |
| | | |
| | | // Use disconnect. |
| | | { new CloseNotify(false, false, false, false) }, |
| | | { new CloseNotify(false, false, false, true) }, |
| | | { new CloseNotify(false, false, true, false) }, |
| | | { new CloseNotify(false, false, true, true) }, |
| | | { new CloseNotify(false, true, false, false) }, |
| | | { new CloseNotify(false, true, false, true) }, |
| | | { new CloseNotify(false, true, true, false) }, |
| | | { new CloseNotify(false, true, true, true) }, |
| | | }; |
| | | // @formatter:on |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | @Test(dataProvider = "closeNotifyConfig") |
| | | public void testCloseNotify(final CloseNotify config) throws Exception { |
| | | final CountDownLatch connectLatch = new CountDownLatch(1); |
| | | final AtomicReference<LDAPClientContext> contextHolder = |
| | | new AtomicReference<LDAPClientContext>(); |
| | | |
| | | final ServerConnectionFactory<LDAPClientContext, Integer> mockServer = |
| | | mock(ServerConnectionFactory.class); |
| | | when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer( |
| | | new Answer<ServerConnection<Integer>>() { |
| | | |
| | | public ServerConnection<Integer> answer(InvocationOnMock invocation) |
| | | throws Throwable { |
| | | // Allow the context to be accessed from outside the mock. |
| | | contextHolder.set((LDAPClientContext) invocation.getArguments()[0]); |
| | | connectLatch.countDown(); /* is this needed? */ |
| | | if (config.closeOnAccept) { |
| | | throw newErrorResult(ResultCode.UNAVAILABLE); |
| | | } else { |
| | | // Return a mock connection which always succeeds for binds. |
| | | ServerConnection<Integer> mockConnection = mock(ServerConnection.class); |
| | | doAnswer(new Answer<Void>() { |
| | | @Override |
| | | public Void answer(InvocationOnMock invocation) throws Throwable { |
| | | ResultHandler<? super BindResult> resultHandler = |
| | | (ResultHandler<? super BindResult>) invocation |
| | | .getArguments()[4]; |
| | | resultHandler.handleResult(Responses |
| | | .newBindResult(ResultCode.SUCCESS)); |
| | | return null; |
| | | } |
| | | }).when(mockConnection).handleBind(anyInt(), anyInt(), |
| | | any(BindRequest.class), any(IntermediateResponseHandler.class), |
| | | any(ResultHandler.class)); |
| | | return mockConnection; |
| | | } |
| | | } |
| | | }); |
| | | |
| | | final int port = TestCaseUtils.findFreePort(); |
| | | LDAPListener listener = new LDAPListener(port, mockServer); |
| | | try { |
| | | LDAPConnectionFactory clientFactory = new LDAPConnectionFactory("localhost", port); |
| | | final Connection client = clientFactory.getConnection(); |
| | | connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | MockConnectionEventListener mockListener = null; |
| | | try { |
| | | if (config.useEventListener) { |
| | | mockListener = new MockConnectionEventListener(); |
| | | client.addConnectionEventListener(mockListener); |
| | | } |
| | | if (config.doBindFirst) { |
| | | client.bind("cn=test", "password".toCharArray()); |
| | | } |
| | | if (!config.closeOnAccept) { |
| | | // Disconnect using client context. |
| | | LDAPClientContext context = contextHolder.get(); |
| | | assertThat(context).isNotNull(); |
| | | assertThat(context.isClosed()).isFalse(); |
| | | if (config.sendDisconnectNotification) { |
| | | context.disconnect(ResultCode.BUSY, "busy"); |
| | | } else { |
| | | context.disconnect(); |
| | | } |
| | | assertThat(context.isClosed()).isTrue(); |
| | | } |
| | | // Block until remote close is signalled. |
| | | if (mockListener != null) { |
| | | // Block using listener. |
| | | mockListener.awaitError(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | assertThat(mockListener.getInvocationCount()).isEqualTo(1); |
| | | assertThat(mockListener.isDisconnectNotification()).isEqualTo( |
| | | config.sendDisconnectNotification); |
| | | assertThat(mockListener.getError()).isNotNull(); |
| | | } else { |
| | | // Block by spinning on isValid. |
| | | waitForCondition(new Callable<Boolean>() { |
| | | @Override |
| | | public Boolean call() throws Exception { |
| | | return !client.isValid(); |
| | | } |
| | | }); |
| | | } |
| | | assertThat(client.isValid()).isFalse(); |
| | | assertThat(client.isClosed()).isFalse(); |
| | | } finally { |
| | | client.close(); |
| | | } |
| | | // Check state after remote close and local close. |
| | | assertThat(client.isValid()).isFalse(); |
| | | assertThat(client.isClosed()).isTrue(); |
| | | if (mockListener != null) { |
| | | mockListener.awaitClose(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | assertThat(mockListener.getInvocationCount()).isEqualTo(2); |
| | | } |
| | | } finally { |
| | | listener.close(); |
| | | } |
| | | } |
| | | |
| | | @SuppressWarnings("unchecked") |
| | | @Test |
| | | public void testUnsolicitedNotifications() throws Exception { |
| | | final CountDownLatch connectLatch = new CountDownLatch(1); |
| | | final AtomicReference<LDAPClientContext> contextHolder = |
| | | new AtomicReference<LDAPClientContext>(); |
| | | |
| | | final ServerConnectionFactory<LDAPClientContext, Integer> mockServer = |
| | | mock(ServerConnectionFactory.class); |
| | | when(mockServer.handleAccept(any(LDAPClientContext.class))).thenAnswer( |
| | | new Answer<ServerConnection<Integer>>() { |
| | | |
| | | public ServerConnection<Integer> answer(InvocationOnMock invocation) |
| | | throws Throwable { |
| | | // Allow the context to be accessed from outside the mock. |
| | | contextHolder.set((LDAPClientContext) invocation.getArguments()[0]); |
| | | connectLatch.countDown(); /* is this needed? */ |
| | | return mock(ServerConnection.class); |
| | | } |
| | | }); |
| | | |
| | | final int port = TestCaseUtils.findFreePort(); |
| | | LDAPListener listener = new LDAPListener(port, mockServer); |
| | | try { |
| | | LDAPConnectionFactory clientFactory = new LDAPConnectionFactory("localhost", port); |
| | | final Connection client = clientFactory.getConnection(); |
| | | connectLatch.await(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | try { |
| | | MockConnectionEventListener mockListener = new MockConnectionEventListener(); |
| | | client.addConnectionEventListener(mockListener); |
| | | |
| | | // Send notification. |
| | | LDAPClientContext context = contextHolder.get(); |
| | | assertThat(context).isNotNull(); |
| | | context.sendUnsolicitedNotification(Responses.newGenericExtendedResult( |
| | | ResultCode.OTHER).setOID("1.2.3.4")); |
| | | assertThat(context.isClosed()).isFalse(); |
| | | |
| | | // Block using listener. |
| | | mockListener.awaitNotification(TEST_TIMEOUT, TimeUnit.SECONDS); |
| | | assertThat(mockListener.getInvocationCount()).isEqualTo(1); |
| | | assertThat(mockListener.getNotification()).isNotNull(); |
| | | assertThat(mockListener.getNotification().getResultCode()).isEqualTo( |
| | | ResultCode.OTHER); |
| | | assertThat(mockListener.getNotification().getOID()).isEqualTo("1.2.3.4"); |
| | | assertThat(client.isValid()).isTrue(); |
| | | assertThat(client.isClosed()).isFalse(); |
| | | } finally { |
| | | client.close(); |
| | | } |
| | | } finally { |
| | | listener.close(); |
| | | } |
| | | } |
| | | |
| | | private void waitForCondition(Callable<Boolean> condition) throws Exception { |
| | | long timeout = System.currentTimeMillis() + TEST_TIMEOUT_MS; |
| | | while (!condition.call()) { |
| | | Thread.yield(); |
| | | if (System.currentTimeMillis() > timeout) { |
| | | throw new TimeoutException("Test timed out after " + TEST_TIMEOUT + " seconds"); |
| | | } |
| | | } |
| | | } |
| | | } |