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

Valery Kharseko
10 hours ago 016502d22e0490ed8b1b4951f5e12be01c4a224c
opendj-server-legacy/src/test/java/org/openidentityplatform/opendj/AliasTestCase.java
@@ -21,6 +21,7 @@
import org.forgerock.opendj.ldap.controls.PersistentSearchRequestControl;
import org.forgerock.opendj.ldap.requests.Requests;
import org.forgerock.opendj.ldap.requests.SearchRequest;
import org.forgerock.opendj.ldap.responses.Result;
import org.forgerock.opendj.ldap.responses.SearchResultEntry;
import org.forgerock.opendj.ldap.responses.SearchResultReference;
import org.forgerock.opendj.ldif.ConnectionEntryReader;
@@ -32,6 +33,9 @@
import org.opends.server.api.LocalBackend;
import org.opends.server.backends.MemoryBackend;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PersistentSearch;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.types.AcceptRejectWarn;
import org.opends.server.types.Entry;
import org.testng.annotations.AfterClass;
@@ -562,7 +566,9 @@
            psearch.searchAsync(request, new SearchResultHandler() {
                @Override
                public boolean handleEntry(SearchResultEntry entry) {
                    notified.add(entry.getName().toString());
                    // Every notification carries the same DN, so the DN alone cannot tell a lost
                    // notification from a duplicated one: record which change is being reported.
                    notified.add(entry.getName() + " " + entry.parseAttribute("description").asString());
                    return true;
                }
@@ -574,25 +580,150 @@
            // searchAsync returns before the server has registered the persistent search, so wait
            // until the backend reports it; otherwise the first modification below can be notified
            // before the search is listening and be missed.
            // before the search is listening and be missed. A failed test is rerun in the same JVM
            // (rerunFailingTestsCount), which can leave the persistent search of the previous run
            // behind, hence the wait for a persistent search on our own base DN.
            final LocalBackend<?> backend = TestCaseUtils.getServerContext()
                    .getBackendConfigManager().getLocalBackendById(TestCaseUtils.TEST_BACKEND_ID);
            for (int i = 0; backend.getPersistentSearches().isEmpty() && i < 500; i++) {
            for (int i = 0; !isPersistentSearchRegistered(backend, "ou=psearch,o=test") && i < 500; i++) {
                Thread.sleep(10);
            }
            assertThat(backend.getPersistentSearches()).isNotEmpty();
            assertThat(isPersistentSearchRegistered(backend, "ou=psearch,o=test"))
                    .as("the persistent search was never registered with backend %s", backend.getBackendID())
                    .isTrue();
            // The same entry is modified repeatedly: each change must reach the persistent search.
            for (int i = 1; i <= 3; i++) {
                connection.modify(Requests.newModifyRequest("cn=changing,ou=psearch,o=test")
                        .addModification(ModificationType.REPLACE, "description", "change " + i));
                assertThat(notified.poll(30, TimeUnit.SECONDS))
                        .as("notification for change " + i)
                        .isEqualTo("cn=changing,ou=psearch,o=test");
                        .as("notification for change %d, persistent search still registered: %s, "
                                        + "notifications received afterwards: %s",
                                i, isPersistentSearchRegistered(backend, "ou=psearch,o=test"), notified)
                        .isEqualTo("cn=changing,ou=psearch,o=test change " + i);
            }
        }
    }
    // A persistent search notification is not a search result: it must be reported whether or not
    // the entry was returned before, and it is not bound by the size and time limits of the search.
    // In a real persistent search the search phase is only open for a few instructions after the
    // search is registered with the backend, so the notification path is driven directly here.
    @Test
    public void test_persistent_search_notification_ignores_search_phase_state() throws Exception {
        TestCaseUtils.addEntries(
                "dn: ou=psearch-notify,o=test",
                "objectClass: top",
                "objectClass: organizationalUnit",
                "ou: psearch-notify",
                ""
        );
        final Entry entry = DirectoryServer.getEntry(DN.valueOf("ou=psearch-notify,o=test"));
        final InternalSearchOperation search = new InternalSearchOperation(
                InternalClientConnection.getRootConnection(),
                InternalClientConnection.nextOperationID(),
                InternalClientConnection.nextMessageID(),
                org.opends.server.protocols.internal.Requests
                        .newSearchRequest(DN.valueOf("o=test"), SearchScope.WHOLE_SUBTREE)
                        .setDereferenceAliasesPolicy(DereferenceAliasesPolicy.ALWAYS));
        // The search phase returns the entry once, and drops it when it reaches it a second time
        // through an alias ...
        assertThat(search.returnEntry(entry, null)).isTrue();
        assertThat(search.returnEntry(entry, null)).isTrue();
        assertThat(search.getSearchEntries()).hasSize(1);
        // ... but a change reported to a persistent search is never a duplicate, whether the search
        // phase is still open (the entry was just returned by it) or already over.
        assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue();
        search.endSearchPhase();
        assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue();
        assertThat(search.getSearchEntries()).hasSize(3);
        // The size limit of the search does not bound a notification: it only bounds the search
        // phase, and is lifted for the rest of a persistent search once that phase is over.
        search.setSizeLimit(1);
        assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue();
        assertThat(search.getSearchEntries()).hasSize(4);
        // The search phase itself is still bound by it.
        assertThat(search.returnEntry(entry, null)).isFalse();
        assertThat(search.getResultCode()).isEqualTo(ResultCode.SIZE_LIMIT_EXCEEDED);
        // Same for the time limit, checked on its own: with a size limit left in the way the search
        // phase would stop on that one and the time limit would never be reached.
        search.setSizeLimit(0);
        search.setTimeLimit(1);
        search.setTimeLimitExpiration(0);
        assertThat(search.returnPersistentSearchEntry(entry, null)).isTrue();
        assertThat(search.getSearchEntries()).hasSize(5);
        assertThat(search.returnEntry(entry, null)).isFalse();
        assertThat(search.getResultCode()).isEqualTo(ResultCode.TIME_LIMIT_EXCEEDED);
    }
    // A persistent search which is cancelled because its backend goes away, as happens when the
    // backend is disabled or re-initialized, must be told so: without a search result done the
    // client waits forever for changes on a search which no longer exists.
    @Test
    public void test_persistent_search_is_told_when_its_backend_goes_away() throws Exception {
        final String backendID = "psearchUnavailable";
        final String baseDN = "o=psearch-unavailable";
        TestCaseUtils.initializeMemoryBackend(backendID, baseDN, true);
        final MemoryBackend backend = (MemoryBackend) TestCaseUtils.getServerContext()
                .getBackendConfigManager().getLocalBackendById(backendID);
        final SearchRequest request =
                Requests.newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, "(objectclass=*)")
                        .addControl(PersistentSearchRequestControl.newControl(
                                true, true, false, PersistentSearchChangeType.MODIFY));
        final LDAPConnectionFactory factory =
                new LDAPConnectionFactory("localhost", TestCaseUtils.getServerLdapPort());
        try (Connection psearch = factory.getConnection()) {
            psearch.bind("cn=Directory Manager", "password".toCharArray());
            final LdapPromise<Result> searchDone = psearch.searchAsync(request, new SearchResultHandler() {
                @Override
                public boolean handleEntry(SearchResultEntry entry) {
                    return true;
                }
                @Override
                public boolean handleReference(SearchResultReference reference) {
                    return true;
                }
            });
            for (int i = 0; !isPersistentSearchRegistered(backend, baseDN) && i < 500; i++) {
                Thread.sleep(10);
            }
            assertThat(isPersistentSearchRegistered(backend, baseDN))
                    .as("the persistent search was never registered with backend %s", backendID)
                    .isTrue();
            backend.finalizeBackend();
            try {
                final Result result = searchDone.getOrThrow(30, TimeUnit.SECONDS);
                fail("the persistent search should have been terminated, it returned " + result);
            } catch (LdapException e) {
                assertThat(e.getResult().getResultCode()).isEqualTo(ResultCode.UNAVAILABLE);
                assertThat(e.getResult().getDiagnosticMessage()).contains(backendID);
            }
        } finally {
            TestCaseUtils.getServerContext().getBackendConfigManager().deregisterLocalBackend(backend);
        }
    }
    /** Whether the provided backend has a persistent search registered for the provided base DN. */
    private static boolean isPersistentSearchRegistered(LocalBackend<?> backend, String baseDN) {
        for (PersistentSearch psearch : backend.getPersistentSearches()) {
            if (psearch.getSearchOperation().getBaseDN().equals(DN.valueOf(baseDN))) {
                return true;
            }
        }
        return false;
    }
    // An alias is dereferenced before its target is reached on its own: the target must still be
    // returned, and exactly once. The original regression was order-sensitive, dropping the target
    // when the alias reached it first, so this pins the alias-before-target order specifically. The