mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Nicolas Capponi
27.28.2013 617b205225d505fc6a17d792b2d3d183aa080321
Fix OPENDJ-972 - OpenDJ should set size limit to 1 when performing single entry searches
Review CR-2378

Main changes in AbstractConnection class:
* where a single entry search / read method constructs a new search request, ensure that it specifies a size limit of 1
* where a single entry search / read method is passed in a search request, check to see if it has a size limit of 1. If not, then duplicate the search request and set the size limit to 1
* detect size limit exceeded errors when processing the results of a single entry search / read : it is then transformed into a client-side error

To implement these changes :
* took into account the server error ResultCode.SIZE_LIMIT_EXCEEDED
* moved behavior for handling errors into SingleEntryHandler
* refactored SingleEntryFuture to use internally a SingleEntryHandler
* simplified searchSingleEntry method to use SingleEntryHandler
* added a new message ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT to handle case where there is too many entries but without the actual number of entries
* and for tests
- added several new tests testSingleEntrySearchXXX in AbstractAsynchronousConnectionTestCase to test single entry search behavior
- added support for the client-provided handler in the internal class MockConnection of AbstractAsynchronousConnectionTestCase to better test behavior in test methods.

Also added support code for creating single entry request
* Added 2 new methods Requests#newSingleEntrySearchRequest to create single entry search requests
* Added a new method SearchRequest#isSingleEntrySearch to test if a request correspond to a single entry search
* Added a new test case class for search requests : SearchRequestTestCase



1 files added
9 files modified
504 ■■■■ changed files
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java 154 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java 56 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java 17 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java 7 ●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java 7 ●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties 2 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties 4 ●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java 162 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java 13 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java 82 ●●●●● patch | view | raw | blame | history
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/AbstractConnection.java
@@ -29,7 +29,9 @@
import static com.forgerock.opendj.ldap.CoreMessages.ERR_NO_SEARCH_RESULT_ENTRIES;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT;
import static com.forgerock.opendj.ldap.CoreMessages.ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import java.util.Collection;
@@ -67,11 +69,7 @@
            SearchResultHandler {
        private final ResultHandler<? super SearchResultEntry> handler;
        private volatile SearchResultEntry firstEntry = null;
        private volatile SearchResultReference firstReference = null;
        private volatile int entryCount = 0;
        private final SingleEntryHandler singleEntryHandler = new SingleEntryHandler();
        private volatile FutureResult<Result> future = null;
@@ -86,14 +84,22 @@
        @Override
        public SearchResultEntry get() throws ErrorResultException, InterruptedException {
            future.get();
            try {
                future.get();
            } catch (ErrorResultException e) {
                throw singleEntryHandler.filterError(e);
            }
            return get0();
        }
        @Override
        public SearchResultEntry get(final long timeout, final TimeUnit unit)
                throws ErrorResultException, TimeoutException, InterruptedException {
            future.get(timeout, unit);
        public SearchResultEntry get(final long timeout, final TimeUnit unit) throws ErrorResultException,
                TimeoutException, InterruptedException {
            try {
                future.get(timeout, unit);
            } catch (ErrorResultException e) {
                throw singleEntryHandler.filterError(e);
            }
            return get0();
        }
@@ -104,26 +110,20 @@
        @Override
        public boolean handleEntry(final SearchResultEntry entry) {
            if (firstEntry == null) {
                firstEntry = entry;
            }
            entryCount++;
            return true;
            return singleEntryHandler.handleEntry(entry);
        }
        @Override
        public void handleErrorResult(final ErrorResultException error) {
            if (handler != null) {
                handler.handleErrorResult(error);
                ErrorResultException finalError = singleEntryHandler.filterError(error);
                handler.handleErrorResult(finalError);
            }
        }
        @Override
        public boolean handleReference(final SearchResultReference reference) {
            if (firstReference == null) {
                firstReference = reference;
            }
            return true;
            return singleEntryHandler.handleReference(reference);
        }
        @Override
@@ -148,21 +148,11 @@
        }
        private SearchResultEntry get0() throws ErrorResultException {
            if (entryCount == 0) {
                // Did not find any entries.
                throw newErrorResult(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
                        ERR_NO_SEARCH_RESULT_ENTRIES.get().toString());
            } else if (entryCount > 1) {
                // Got more entries than expected.
                throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(entryCount).toString());
            } else if (firstReference != null) {
                // Got an unexpected search result reference.
                throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                        ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(
                                firstReference.getURIs().iterator().next()).toString());
            ErrorResultException exception = singleEntryHandler.checkForClientSideError();
            if (exception == null) {
                return singleEntryHandler.firstEntry;
            } else {
                return firstEntry;
                throw exception;
            }
        }
@@ -192,7 +182,7 @@
         */
        @Override
        public void handleErrorResult(final ErrorResultException error) {
            // Ignore.
            // Ignore
        }
        @Override
@@ -211,6 +201,50 @@
            // Ignore.
        }
        /**
         * Filter the provided error in order to transform size limit exceeded error to a client side error,
         * or leave it as is for any other error.
         *
         * @param error to filter
         * @return provided error in most case, or <code>ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED</code>
         * error if provided error is <code>ResultCode.SIZE_LIMIT_EXCEEDED</code>
         */
        public ErrorResultException filterError(final ErrorResultException error) {
            if (error.getResult().getResultCode().equals(ResultCode.SIZE_LIMIT_EXCEEDED)) {
                return newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT.get().toString());
            } else {
                return error;
            }
        }
        /**
         * Check for any error related to number of search result at client-side level: no result,
         * too many result, search result reference.
         *
         * This method should be called only after search operation is finished.
         *
         * @return an <code>ErrorResultException</code> if an error is detected, <code>null</code> otherwise
         */
        public ErrorResultException checkForClientSideError() {
            ErrorResultException exception = null;
            if (entryCount == 0) {
                // Did not find any entries.
                exception = newErrorResult(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED, ERR_NO_SEARCH_RESULT_ENTRIES
                        .get().toString());
            } else if (entryCount > 1) {
                // Got more entries than expected.
                exception = newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                        ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(entryCount).toString());
            } else if (firstReference != null) {
                // Got an unexpected search result reference.
                exception = newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                        ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(firstReference.getURIs().iterator().next())
                                .toString());
            }
            return exception;
        }
    }
    // Visitor used for processing synchronous change requests.
@@ -410,7 +444,7 @@
    public SearchResultEntry readEntry(final DN baseObject, final String... attributeDescriptions)
            throws ErrorResultException {
        final SearchRequest request =
                Requests.newSearchRequest(baseObject, SearchScope.BASE_OBJECT, Filter
                Requests.newSingleEntrySearchRequest(baseObject, SearchScope.BASE_OBJECT, Filter
                        .objectClassPresent(), attributeDescriptions);
        return searchSingleEntry(request);
    }
@@ -432,8 +466,9 @@
            final Collection<String> attributeDescriptions,
            final ResultHandler<? super SearchResultEntry> handler) {
        final SearchRequest request =
                Requests.newSearchRequest(name, SearchScope.BASE_OBJECT, Filter
                        .objectClassPresent());
                Requests.newSingleEntrySearchRequest(
                        name, SearchScope.BASE_OBJECT,
                        Filter.objectClassPresent());
        if (attributeDescriptions != null) {
            request.getAttributes().addAll(attributeDescriptions);
        }
@@ -521,23 +556,16 @@
    public SearchResultEntry searchSingleEntry(final SearchRequest request)
            throws ErrorResultException {
        final SingleEntryHandler handler = new SingleEntryHandler();
        search(request, handler);
        if (handler.entryCount == 0) {
            // Did not find any entries.
            throw newErrorResult(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED,
                    ERR_NO_SEARCH_RESULT_ENTRIES.get().toString());
        } else if (handler.entryCount > 1) {
            // Got more entries than expected.
            throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                    ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES.get(handler.entryCount).toString());
        } else if (handler.firstReference != null) {
            // Got an unexpected search result reference.
            throw newErrorResult(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED,
                    ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES.get(
                            handler.firstReference.getURIs().iterator().next()).toString());
        } else {
        try {
            search(enforceSingleEntrySearchRequest(request), handler);
        } catch (ErrorResultException e) {
            throw handler.filterError(e);
        }
        ErrorResultException error = handler.checkForClientSideError();
        if (error == null) {
            return handler.firstEntry;
        } else {
            throw error;
        }
    }
@@ -548,7 +576,7 @@
    public SearchResultEntry searchSingleEntry(final String baseObject, final SearchScope scope,
            final String filter, final String... attributeDescriptions) throws ErrorResultException {
        final SearchRequest request =
                Requests.newSearchRequest(baseObject, scope, filter, attributeDescriptions);
                Requests.newSingleEntrySearchRequest(baseObject, scope, filter, attributeDescriptions);
        return searchSingleEntry(request);
    }
@@ -559,12 +587,28 @@
    public FutureResult<SearchResultEntry> searchSingleEntryAsync(final SearchRequest request,
            final ResultHandler<? super SearchResultEntry> handler) {
        final SingleEntryFuture innerFuture = new SingleEntryFuture(handler);
        final FutureResult<Result> future = searchAsync(request, null, innerFuture);
        final FutureResult<Result> future =
                searchAsync(enforceSingleEntrySearchRequest(request), null, innerFuture);
        innerFuture.setResultFuture(future);
        return innerFuture;
    }
    /**
     * Ensure that a single entry search request is returned, based on provided request.
     *
     * @param request
     *            to be checked
     * @return a single entry search request, equal to or based on the provided request
     */
    private SearchRequest enforceSingleEntrySearchRequest(final SearchRequest request) {
        if (request.isSingleEntrySearch()) {
            return request;
        } else {
            return Requests.copyOfSearchRequest(request).setSizeLimit(1);
        }
    }
    /**
     * {@inheritDoc}
     * <p>
     * Sub-classes should provide an implementation which returns an appropriate
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/Requests.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2011-2012 ForgeRock AS
 *      Portions copyright 2011-2013 ForgeRock AS
 */
package org.forgerock.opendj.ldap.requests;
@@ -1017,7 +1017,7 @@
    /**
     * Creates a new search request using the provided distinguished name,
     * scope, and filter, decoded using the default schema.
     * scope, and filter.
     *
     * @param name
     *            The distinguished name of the base entry relative to which the
@@ -1079,6 +1079,58 @@
    }
    /**
     * Creates a new search request for a single entry, using the provided distinguished name,
     * scope, and filter.
     *
     * @param name
     *            The distinguished name of the base entry relative to which the
     *            search is to be performed.
     * @param scope
     *            The scope of the search.
     * @param filter
     *            The filter that defines the conditions that must be fulfilled
     *            in order for an entry to be returned.
     * @param attributeDescriptions
     *            The names of the attributes to be included with each entry.
     * @return The new search request.
     * @throws NullPointerException
     *             If the {@code name}, {@code scope}, or {@code filter} were
     *             {@code null}.
     */
    public static SearchRequest newSingleEntrySearchRequest(final DN name, final SearchScope scope,
            final Filter filter, final String... attributeDescriptions) {
        return newSearchRequest(name, scope, filter, attributeDescriptions).setSizeLimit(1);
    }
    /**
     * Creates a new search request for a single entry, using the provided distinguished name,
     * scope, and filter, decoded using the default schema.
     *
     * @param name
     *            The distinguished name of the base entry relative to which the
     *            search is to be performed.
     * @param scope
     *            The scope of the search.
     * @param filter
     *            The filter that defines the conditions that must be fulfilled
     *            in order for an entry to be returned.
     * @param attributeDescriptions
     *            The names of the attributes to be included with each entry.
     * @return The new search request.
     * @throws LocalizedIllegalArgumentException
     *             If {@code name} could not be decoded using the default
     *             schema, or if {@code filter} is not a valid LDAP string
     *             representation of a filter.
     * @throws NullPointerException
     *             If the {@code name}, {@code scope}, or {@code filter} were
     *             {@code null}.
     */
    public static SearchRequest newSingleEntrySearchRequest(final String name, final SearchScope scope,
            final String filter, final String... attributeDescriptions) {
        return newSearchRequest(name, scope, filter, attributeDescriptions).setSizeLimit(1);
    }
    /**
     * Creates a new simple bind request having an empty name and password
     * suitable for anonymous authentication.
     *
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequest.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2009-2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
@@ -147,6 +147,19 @@
    int getSizeLimit();
    /**
     * Indicates whether search result is expected to be limited to a single entry.
     * <p>
     * It is the case if size limit is equal to 1 or if scope is equal to <code>SearchScope.BASE_OBJECT</code>.
     * <p>
     * If search results contain more than one entry, the search operation will throw
     * a <code>MultipleEntriesFoundException</code>.
     *
     * @return {@code true} if the search is limited to a single entry result,
     *         or {@code false} (the default) otherwise.
     */
    boolean isSingleEntrySearch();
    /**
     * Returns the time limit that should be used in order to restrict the
     * maximum time (in seconds) allowed for the search.
     * <p>
@@ -270,6 +283,8 @@
     * A value of zero (the default) in this field indicates that no
     * client-requested size limit restrictions are in effect. Servers may also
     * enforce a maximum number of entries to return.
     * <p>
     * This method overrides the size limit set using a previous call to {@link #setSingleEntrySearch()}.
     *
     * @param limit
     *            The size limit that should be used in order to restrict the
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/SearchRequestImpl.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
@@ -107,6 +107,11 @@
    }
    @Override
    public boolean isSingleEntrySearch() {
        return sizeLimit == 1 || SearchScope.BASE_OBJECT.equals(scope);
    }
    @Override
    public int getTimeLimit() {
        return timeLimit;
    }
opendj3/opendj-ldap-sdk/src/main/java/org/forgerock/opendj/ldap/requests/UnmodifiableSearchRequestImpl.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
@@ -80,6 +80,11 @@
    }
    @Override
    public boolean isSingleEntrySearch() {
        return impl.isSingleEntrySearch();
    }
    @Override
    public int getTimeLimit() {
        return impl.getTimeLimit();
    }
opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core.properties
@@ -844,6 +844,8 @@
 but did not return any search result entries when one was expected
ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES=The search request succeeded \
 but returned %d search result entry when only one was expected
ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT=The search request succeeded \
 but returned more than one search result entry when only one was expected
ERR_UNEXPECTED_SEARCH_RESULT_REFERENCES=The search request succeeded \
 but returned a search result reference containing the following URI: %s
#
opendj3/opendj-ldap-sdk/src/main/resources/com/forgerock/opendj/ldap/core_fr.properties
@@ -20,6 +20,7 @@
# CDDL HEADER END
#
#      Copyright 2009 Sun Microsystems, Inc.
#      Portions copyright 2013 ForgeRock AS
#
ERR_ATTR_SYNTAX_COUNTRY_STRING_INVALID_LENGTH_9=La valeur indiqu\u00e9e "%s" n'est pas une cha\u00eene de pays valide car elle n'a pas une longueur de deux caract\u00e8res exactement
ERR_ATTR_SYNTAX_DELIVERY_METHOD_NO_ELEMENTS_11=La valeur indiqu\u00e9e "%s" n'est pas une m\u00e9thode de distribution valide car elle ne contient aucun \u00e9l\u00e9ment
@@ -94,3 +95,6 @@
ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_STRUCTURAL_271=La r\u00e8gle de contenu DIT "%s" n'est pas valide car elle interdit l'utilisation du type d'attribut %s requis par la classe d'objet structurelle associ\u00e9e %s
ERR_ATTR_SYNTAX_DCR_PROHIBITED_REQUIRED_BY_AUXILIARY_272=La r\u00e8gle de contenu DIT "%s" n'est pas valide car elle interdit l'utilisation du type d'attribut %s requis par la classe d'objet structurelle auxiliaire %s
ERR_ATTR_SYNTAX_DN_INVALID_REQUIRES_ESCAPE_CHAR_282=Impossible d'analyser la valeur fournie "%s" en tant que nom valide distinctif car la valeur de l'attribut commence avec un caract\u00e8re en position %d qui doit \u00eatre \u00e9vit\u00e9
ERR_NO_SEARCH_RESULT_ENTRIES=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais n'a retourn\u00e9 aucune entr\u00e9e alors qu'une entr\u00e9e \u00e9tait attendue
ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais a retourn\u00e9 %s entr\u00e9es alors qu'une seule entr\u00e9e \u00e9tait attendue
ERR_UNEXPECTED_SEARCH_RESULT_ENTRIES_NO_COUNT=La recherche a \u00e9t\u00e9 effectu\u00e9e avec succ\u00e8s mais a retourn\u00e9 plusieurs entr\u00e9es alors qu'une seule entr\u00e9e \u00e9tait attendue
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/AbstractAsynchronousConnectionTestCase.java
@@ -22,7 +22,7 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2011 ForgeRock AS.
 *      Portions copyright 2011-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap;
@@ -30,6 +30,7 @@
import static org.fest.assertions.Assertions.assertThat;
import static org.fest.assertions.Fail.fail;
import static org.forgerock.opendj.ldap.ErrorResultException.newErrorResult;
import static org.mockito.Mockito.*;
import java.util.LinkedList;
import java.util.List;
@@ -65,15 +66,11 @@
    private final class MockConnection extends AbstractAsynchronousConnection {
        private final ResultCode resultCode;
        private final SearchResultEntry entry;
        private final SearchResultEntry[] entries;
        private MockConnection(ResultCode resultCode) {
            this(resultCode, null);
        }
        private MockConnection(ResultCode resultCode, SearchResultEntry entry) {
        private MockConnection(ResultCode resultCode, SearchResultEntry...entries) {
            this.resultCode = resultCode;
            this.entry = entry;
            this.entries = entries;
        }
        /**
@@ -222,14 +219,17 @@
        public FutureResult<Result> searchAsync(SearchRequest request,
                IntermediateResponseHandler intermediateResponseHandler,
                SearchResultHandler resultHandler) {
            if (entry != null) {
            for (SearchResultEntry entry : entries) {
                resultHandler.handleEntry(entry);
            }
            if (!resultCode.isExceptional()) {
                return new CompletedFutureResult<Result>(Responses.newResult(resultCode));
            if (resultCode.isExceptional()) {
                ErrorResultException errorResult = newErrorResult(resultCode);
                resultHandler.handleErrorResult(errorResult);
                return new CompletedFutureResult<Result>(errorResult);
            } else {
                return new CompletedFutureResult<Result>(newErrorResult(resultCode));
                Result result = Responses.newResult(resultCode);
                resultHandler.handleResult(result);
                return new CompletedFutureResult<Result>(result);
            }
        }
@@ -401,10 +401,144 @@
        List<SearchResultEntry> entries = new LinkedList<SearchResultEntry>();
        try {
            mockConnection.search(searchRequest, entries);
            fail();
            failWasExpected(ErrorResultException.class);
        } catch (ErrorResultException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
            assertThat(entries.isEmpty());
        }
    }
    @Test()
    public void testSingleEntrySearchRequestSuccess() throws Exception {
        final SearchResultEntry entry = Responses.newSearchResultEntry("cn=test");
        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        assertThat(mockConnection.searchSingleEntry(request)).isEqualTo(entry);
    }
    @SuppressWarnings("unchecked")
    @Test()
    public void testSingleEntrySearchAsyncRequestSuccess() throws Exception {
        final SearchResultEntry entry = Responses.newSearchResultEntry("cn=test");
        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS, entry);
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
        FutureResult<SearchResultEntry> futureResult = mockConnection.searchSingleEntryAsync(request, handler);
        assertThat(futureResult.get()).isEqualTo(entry);
        verify(handler).handleResult(any(SearchResultEntry.class));
    }
    @Test()
    public void testSingleEntrySearchRequestNoEntryReturned() throws Exception {
        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS);
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        try {
            mockConnection.searchSingleEntry(request);
            failWasExpected(EntryNotFoundException.class);
        } catch (EntryNotFoundException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_NO_RESULTS_RETURNED);
        }
    }
    @Test()
    public void testSingleEntrySearchRequestMultipleEntriesToReturn() throws Exception {
        final Connection mockConnection = new MockConnection(ResultCode.SIZE_LIMIT_EXCEEDED,
                Responses.newSearchResultEntry("cn=test"));
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        try {
            mockConnection.searchSingleEntry(request);
            failWasExpected(MultipleEntriesFoundException.class);
        } catch (MultipleEntriesFoundException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
        }
    }
    @Test()
    public void testSingleEntrySearchRequestMultipleEntriesReturnedByServer() throws Exception {
        // could happen if server does not enforce size limit
        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS,
                Responses.newSearchResultEntry("cn=test"),
                Responses.newSearchResultEntry("cn=test,ou=org"));
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.WHOLE_SUBTREE, "(objectClass=*)");
        try {
            mockConnection.searchSingleEntry(request);
            failWasExpected(MultipleEntriesFoundException.class);
        } catch (MultipleEntriesFoundException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
        }
    }
    @SuppressWarnings("unchecked")
    @Test()
    public void testSingleEntrySearchAsyncRequestMultipleEntriesToReturn() throws Exception {
        final Connection mockConnection = new MockConnection(ResultCode.SIZE_LIMIT_EXCEEDED,
                Responses.newSearchResultEntry("cn=test"));
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
        try {
            mockConnection.searchSingleEntryAsync(request, handler).get();
            failWasExpected(MultipleEntriesFoundException.class);
        } catch (MultipleEntriesFoundException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
            verify(handler).handleErrorResult(any(ErrorResultException.class));
        }
    }
    @Test()
    public void testSingleEntrySearchAsyncRequestMultipleEntriesReturnedByServer() throws Exception {
        // could happen if server does not enfore size limit
        final Connection mockConnection = new MockConnection(ResultCode.SUCCESS,
                Responses.newSearchResultEntry("cn=test"),
                Responses.newSearchResultEntry("cn=test,ou=org"));
        final SearchRequest request = Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT,
                "(objectClass=*)");
        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
        try {
            mockConnection.searchSingleEntryAsync(request, handler).get();
            failWasExpected(MultipleEntriesFoundException.class);
        } catch (MultipleEntriesFoundException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.CLIENT_SIDE_UNEXPECTED_RESULTS_RETURNED);
            verify(handler).handleErrorResult(any(ErrorResultException.class));
        }
    }
    @Test()
    public void testSingleEntrySearchRequestFail() throws Exception {
        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        try {
            mockConnection.searchSingleEntry(request);
            failWasExpected(ErrorResultException.class);
        } catch (ErrorResultException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
        }
    }
    @Test()
    public void testSingleEntrySearchAsyncRequestFail() throws Exception {
        final Connection mockConnection = new MockConnection(ResultCode.UNWILLING_TO_PERFORM);
        final SearchRequest request =
                Requests.newSingleEntrySearchRequest("cn=test", SearchScope.BASE_OBJECT, "(objectClass=*)");
        ResultHandler<SearchResultEntry> handler = mock(ResultHandler.class);
        try {
            mockConnection.searchSingleEntryAsync(request, handler).get();
            failWasExpected(ErrorResultException.class);
        } catch (ErrorResultException e) {
            assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNWILLING_TO_PERFORM);
            verify(handler).handleErrorResult(any(ErrorResultException.class));
        }
    }
}
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/SdkTestCase.java
@@ -22,10 +22,12 @@
 *
 *
 *      Copyright 2010 Sun Microsystems, Inc.
 *      Portions copyright 2012 ForgeRock AS.
 *      Portions copyright 2012-2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap;
import static org.fest.assertions.Fail.*;
import org.forgerock.testng.ForgeRockTestCase;
import org.testng.annotations.Test;
@@ -35,4 +37,13 @@
 */
@Test(groups = { "precommit", "types", "sdk" })
public abstract class SdkTestCase extends ForgeRockTestCase {
    /**
     * Fail with precise message giving the exception that was expected.
     *
     * @param exceptionClass expected exception
     */
    void failWasExpected(Class<? extends Throwable> exceptionClass) {
        fail("should throw an exception " + exceptionClass.getSimpleName());
    }
}
opendj3/opendj-ldap-sdk/src/test/java/org/forgerock/opendj/ldap/requests/SearchRequestTestCase.java
New file
@@ -0,0 +1,82 @@
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License, Version 1.0 only
 * (the "License").  You may not use this file except in compliance
 * with the License.
 *
 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
 * or http://forgerock.org/license/CDDLv1.0.html.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at legal-notices/CDDLv1_0.txt.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information:
 *      Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 *
 *      Copyright 2013 ForgeRock AS.
 */
package org.forgerock.opendj.ldap.requests;
import static org.fest.assertions.Assertions.*;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.Filter;
import org.forgerock.opendj.ldap.SearchScope;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@SuppressWarnings("javadoc")
public class SearchRequestTestCase extends RequestTestCase {
    @DataProvider(name = "SearchRequests")
    public Object[][] getSearchRequests() throws Exception {
        return getTestRequests();
    }
    @Override
    protected SearchRequest[] createTestRequests() throws Exception {
        return new SearchRequest[] {
                Requests.newSearchRequest(
                        "uid=user.0,ou=people,o=test",
                        SearchScope.BASE_OBJECT, "(uid=user)", "uid", "ou"),
                Requests.newSearchRequest("uid=user.0,ou=people,o=test",
                        SearchScope.SINGLE_LEVEL, "(uid=user)", "uid", "ou") };
    }
    @Test
    public void createRequestForSingleEntrySearch() throws Exception {
        SearchRequest request = Requests.newSingleEntrySearchRequest(
                DN.valueOf("uid=user.0,ou=people,o=test"),
                SearchScope.BASE_OBJECT, Filter.equality("uid", "user"), "uid");
        assertThat(request.getSizeLimit()).isEqualTo(1);
        assertThat(request.isSingleEntrySearch()).isTrue();
    }
    @Test
    public void createRequestForSingleEntrySearchWithStrings() throws Exception {
        SearchRequest request = Requests.newSingleEntrySearchRequest(
                "uid=user.0,ou=people,o=test",
                SearchScope.BASE_OBJECT, "(uid=user)", "uid");
        assertThat(request.getSizeLimit()).isEqualTo(1);
        assertThat(request.isSingleEntrySearch()).isTrue();
    }
    @Test
    public void createRequestWithBaseObjectScope() throws Exception {
        SearchRequest request = Requests.newSearchRequest(
                DN.valueOf("uid=user.0,ou=people,o=test"),
                SearchScope.BASE_OBJECT, Filter.equality("uid", "user"), "uid");
        assertThat(request.isSingleEntrySearch()).isTrue();
    }
}