From 2e9b3a57c92eb0e158cb75da50cb9c22a292c6a5 Mon Sep 17 00:00:00 2001
From: Nicolas Capponi <nicolas.capponi@forgerock.com>
Date: Tue, 23 Sep 2014 12:30:48 +0000
Subject: [PATCH] Actual merge of ChangelogBackendTestCase class

---
 opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/ChangelogBackendTestCase.java | 1549 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 1,549 insertions(+), 0 deletions(-)

diff --git a/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/ChangelogBackendTestCase.java b/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/ChangelogBackendTestCase.java
new file mode 100644
index 0000000..cd7c6fd
--- /dev/null
+++ b/opendj3-server-dev/tests/unit-tests-testng/src/server/org/opends/server/backends/ChangelogBackendTestCase.java
@@ -0,0 +1,1549 @@
+/*
+ * 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 2014 ForgeRock AS.
+ */
+package org.opends.server.backends;
+
+import java.io.ByteArrayOutputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.SortedSet;
+
+import org.assertj.core.api.Assertions;
+import org.forgerock.i18n.slf4j.LocalizedLogger;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DereferenceAliasesPolicy;
+import org.forgerock.opendj.ldap.ModificationType;
+import org.forgerock.opendj.ldap.ResultCode;
+import org.forgerock.opendj.ldap.SearchScope;
+import org.opends.server.TestCaseUtils;
+import org.opends.server.admin.std.server.ExternalChangelogDomainCfg;
+import org.opends.server.api.Backend;
+import org.opends.server.backends.ChangelogBackend.SearchParams;
+import org.opends.server.controls.ExternalChangelogRequestControl;
+import org.opends.server.core.DeleteOperation;
+import org.opends.server.core.DirectoryServer;
+import org.opends.server.core.ModifyDNOperation;
+import org.opends.server.core.ModifyDNOperationBasis;
+import org.opends.server.core.ModifyOperation;
+import org.opends.server.protocols.internal.InternalClientConnection;
+import org.opends.server.protocols.internal.InternalSearchListener;
+import org.opends.server.protocols.internal.InternalSearchOperation;
+import org.opends.server.replication.ReplicationTestCase;
+import org.opends.server.replication.common.CSN;
+import org.opends.server.replication.common.CSNGenerator;
+import org.opends.server.replication.common.MultiDomainServerState;
+import org.opends.server.replication.plugin.DomainFakeCfg;
+import org.opends.server.replication.plugin.ExternalChangelogDomainFakeCfg;
+import org.opends.server.replication.plugin.LDAPReplicationDomain;
+import org.opends.server.replication.plugin.MultimasterReplication;
+import org.opends.server.replication.protocol.AddMsg;
+import org.opends.server.replication.protocol.DeleteMsg;
+import org.opends.server.replication.protocol.ModifyDNMsg;
+import org.opends.server.replication.protocol.ModifyDnContext;
+import org.opends.server.replication.protocol.ModifyMsg;
+import org.opends.server.replication.protocol.ReplicationMsg;
+import org.opends.server.replication.protocol.ResetGenerationIdMsg;
+import org.opends.server.replication.protocol.UpdateMsg;
+import org.opends.server.replication.server.ReplServerFakeConfiguration;
+import org.opends.server.replication.server.ReplicationServer;
+import org.opends.server.replication.server.changelog.api.DBCursor;
+import org.opends.server.replication.server.changelog.api.ReplicationDomainDB;
+import org.opends.server.replication.server.changelog.je.ECLEnabledDomainPredicate;
+import org.opends.server.replication.service.DSRSShutdownSync;
+import org.opends.server.replication.service.ReplicationBroker;
+import org.opends.server.types.Attribute;
+import org.opends.server.types.Attributes;
+import org.opends.server.types.AuthenticationInfo;
+import org.opends.server.types.Control;
+import org.opends.server.types.DN;
+import org.opends.server.types.DirectoryException;
+import org.opends.server.types.Entry;
+import org.opends.server.types.LDIFExportConfig;
+import org.opends.server.types.Modification;
+import org.opends.server.types.Operation;
+import org.opends.server.types.RDN;
+import org.opends.server.types.SearchFilter;
+import org.opends.server.types.SearchResultEntry;
+import org.opends.server.util.LDIFWriter;
+import org.opends.server.util.TimeThread;
+import org.opends.server.workflowelement.localbackend.LocalBackendModifyDNOperation;
+import org.testng.annotations.AfterClass;
+import org.testng.annotations.AfterMethod;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+import com.forgerock.opendj.util.Pair;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.forgerock.opendj.ldap.ResultCode.*;
+import static org.opends.messages.ReplicationMessages.*;
+import static org.opends.server.TestCaseUtils.*;
+import static org.opends.server.replication.protocol.OperationContext.*;
+import static org.opends.server.replication.server.changelog.api.DBCursor.KeyMatchingStrategy.*;
+import static org.opends.server.replication.server.changelog.api.DBCursor.PositionStrategy.*;
+import static org.opends.server.util.CollectionUtils.*;
+import static org.opends.server.util.ServerConstants.*;
+import static org.opends.server.util.StaticUtils.*;
+import static org.testng.Assert.*;
+
+@SuppressWarnings("javadoc")
+public class ChangelogBackendTestCase extends ReplicationTestCase
+{
+  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
+
+  private static final String USER1_ENTRY_UUID = "11111111-1111-1111-1111-111111111111";
+  private static final long CHANGENUMBER_ZERO = 0L;
+
+  private static final int SERVER_ID_1 = 1201;
+  private static final int SERVER_ID_2 = 1202;
+
+  private static final String TEST_BACKEND_ID2 = "test2";
+  private static final String TEST_BACKEND_ID3 = "test3";
+  private static final String TEST_ROOT_DN_STRING2 = "o=" + TEST_BACKEND_ID2;
+  private static final String TEST_ROOT_DN_STRING3 = "o=" + TEST_BACKEND_ID3;
+  private static DN DN_OTEST;
+  private static DN DN_OTEST2;
+  private static DN DN_OTEST3;
+
+  private final int brokerSessionTimeout = 5000;
+  private final int maxWindow = 100;
+
+  /** The replicationServer that will be used in this test. */
+  private ReplicationServer replicationServer;
+
+  /** The port of the replicationServer. */
+  private int replicationServerPort;
+
+  /**
+   * When used in a search operation, it includes all attributes (user and
+   * operational)
+   */
+  private static final Set<String> ALL_ATTRIBUTES = newSet("*", "+");
+  private static final List<Control> NO_CONTROL = null;
+
+  @BeforeClass
+  @Override
+  public void setUp() throws Exception
+  {
+    super.setUp();
+    DN_OTEST = DN.valueOf(TEST_ROOT_DN_STRING);
+    DN_OTEST2 = DN.valueOf(TEST_ROOT_DN_STRING2);
+    DN_OTEST3 = DN.valueOf(TEST_ROOT_DN_STRING3);
+
+    // This test suite depends on having the schema available.
+    configureReplicationServer();
+  }
+
+  @Override
+  @AfterClass
+  public void classCleanUp() throws Exception
+  {
+    callParanoiaCheck = false;
+    super.classCleanUp();
+
+    remove(replicationServer);
+    replicationServer = null;
+
+    paranoiaCheck();
+  }
+
+  @AfterMethod
+  public void clearReplicationDb() throws Exception
+  {
+    clearChangelogDB(replicationServer);
+  }
+
+  /** Configure a replicationServer for test. */
+  private void configureReplicationServer() throws Exception
+  {
+    replicationServerPort = TestCaseUtils.findFreePort();
+
+    ReplServerFakeConfiguration config = new ReplServerFakeConfiguration(
+          replicationServerPort,
+          "ChangelogBackendTestDB",
+          replicationDbImplementation,
+          0,         // purge delay
+          71,        // server id
+          0,         // queue size
+          maxWindow, // window size
+          null       // servers
+    );
+    config.setComputeChangeNumber(true);
+    replicationServer = new ReplicationServer(config, new DSRSShutdownSync(), new ECLEnabledDomainPredicate()
+    {
+      @Override
+      public boolean isECLEnabledDomain(DN baseDN)
+      {
+        return baseDN.toString().startsWith("o=test");
+      }
+    });
+    debugInfo("configure", "ReplicationServer created:" + replicationServer);
+  }
+
+  /** Enable replication on provided domain DN and serverid, using provided port. */
+  private Pair<ReplicationBroker, LDAPReplicationDomain> enableReplication(DN domainDN, int serverId,
+      int replicationPort, int timeout) throws Exception
+  {
+    ReplicationBroker broker = openReplicationSession(domainDN, serverId, 100, replicationPort, timeout);
+    DomainFakeCfg domainConf = newFakeCfg(domainDN, serverId, replicationPort);
+    LDAPReplicationDomain replicationDomain = startNewReplicationDomain(domainConf, null, null);
+    return Pair.of(broker, replicationDomain);
+  }
+
+  /** Start a new replication domain on the directory server side. */
+  private LDAPReplicationDomain startNewReplicationDomain(
+      DomainFakeCfg domainConf,
+      SortedSet<String> eclInclude,
+      SortedSet<String> eclIncludeForDeletes)
+          throws Exception
+  {
+    domainConf.setExternalChangelogDomain(new ExternalChangelogDomainFakeCfg(true, eclInclude, eclIncludeForDeletes));
+    // Set a Changetime heartbeat interval low enough
+    // (less than default value that is 1000 ms)
+    // for the test to be sure to consider all changes as eligible.
+    domainConf.setChangetimeHeartbeatInterval(10);
+    LDAPReplicationDomain newDomain = MultimasterReplication.createNewDomain(domainConf);
+    newDomain.start();
+    return newDomain;
+  }
+
+  private void removeReplicationDomains(LDAPReplicationDomain... domains)
+  {
+    for (LDAPReplicationDomain domain : domains)
+    {
+      if (domain != null)
+      {
+        domain.shutdown();
+        MultimasterReplication.deleteDomain(domain.getBaseDN());
+      }
+    }
+  }
+
+  @Test
+  public void searchInCookieModeOnOneSuffixUsingEmptyCookie() throws Exception
+  {
+    String test = "EmptyCookie";
+    debugInfo(test, "Starting test\n\n");
+
+    final CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(test, true);
+    final String[] cookies = buildCookiesFromCsns(csns);
+
+    int nbEntries = 4;
+    String cookie = "";
+    InternalSearchOperation searchOp =
+        searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookie, nbEntries, SUCCESS, test);
+
+    final List<SearchResultEntry> searchEntries = searchOp.getSearchEntries();
+    assertDelEntry(searchEntries.get(0), test + 1, test + "uuid1", CHANGENUMBER_ZERO, csns[0], cookies[0]);
+    assertAddEntry(searchEntries.get(1), test + 2, USER1_ENTRY_UUID, CHANGENUMBER_ZERO, csns[1], cookies[1]);
+    assertModEntry(searchEntries.get(2), test + 3, test + "uuid3", CHANGENUMBER_ZERO, csns[2], cookies[2]);
+    assertModDNEntry(searchEntries.get(3), test + 4, test + "new4", test+"uuid4", CHANGENUMBER_ZERO,
+        csns[3], cookies[3]);
+    assertResultsContainCookieControl(searchOp, cookies);
+
+    assertChangelogAttributesInRootDSE(true, 1, 4);
+
+    debugInfo(test, "Ending search with success");
+  }
+
+  @Test
+  public void searchInCookieModeOnOneSuffix() throws Exception
+  {
+    String test = "CookieOneSuffix";
+    debugInfo(test, "Starting test\n\n");
+    InternalSearchOperation searchOp = null;
+
+    final CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(test, true);
+    final String[] cookies = buildCookiesFromCsns(csns);
+
+    // check querying with cookie of delete entry : should return  3 entries
+    int nbEntries = 3;
+    searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[0], nbEntries, SUCCESS, test);
+
+    List<SearchResultEntry> searchEntries = searchOp.getSearchEntries();
+    assertAddEntry(searchEntries.get(0), test + 2, USER1_ENTRY_UUID, CHANGENUMBER_ZERO, csns[1], cookies[1]);
+    assertModEntry(searchEntries.get(1), test + 3, test + "uuid3", CHANGENUMBER_ZERO, csns[2], cookies[2]);
+    assertModDNEntry(searchEntries.get(2), test + 4, test + "new4", test+"uuid4", CHANGENUMBER_ZERO,
+        csns[3], cookies[3]);
+
+    // check querying with cookie of add entry : should return 2 entries
+    nbEntries = 2;
+    searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[1], nbEntries, SUCCESS, test);
+
+    // check querying with cookie of mod entry : should return 1 entry
+    nbEntries = 1;
+    searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[2], nbEntries, SUCCESS, test);
+
+    searchEntries = searchOp.getSearchEntries();
+    assertModDNEntry(searchEntries.get(0), test + 4, test + "new4", test+"uuid4", CHANGENUMBER_ZERO,
+        csns[3], cookies[3]);
+
+    // check querying with cookie of mod dn entry : should return 0 entry
+    nbEntries = 0;
+    searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[3], nbEntries, SUCCESS, test);
+
+    debugInfo(test, "Ending search with success");
+  }
+
+  @Test
+  public void searchInCookieModeAfterDomainIsRemoved() throws Exception
+  {
+    String test = "CookieAfterDomainIsRemoved";
+    debugInfo(test, "Starting test");
+
+    final CSN[] csns = generateCSNs(3, SERVER_ID_1);
+
+    publishUpdateMessagesInOTest(test, true,
+        generateDeleteMsg(TEST_ROOT_DN_STRING,  csns[0], test, 1),
+        generateDeleteMsg(TEST_ROOT_DN_STRING,  csns[1], test, 2),
+        generateDeleteMsg(TEST_ROOT_DN_STRING,  csns[2], test, 3));
+
+    InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*)", "", 3, SUCCESS, test);
+    String firstCookie = readCookieFromNthEntry(searchOp.getSearchEntries(), 0);
+
+    // remove the domain by sending a reset message
+    publishUpdateMessages(test, DN_OTEST, SERVER_ID_1, false, new ResetGenerationIdMsg(23657));
+
+
+    // replication changelog must have been cleared
+    String cookie= "";
+    searchChangelogUsingCookie("(targetDN=*)", cookie, 0, SUCCESS, test);
+
+    cookie = readLastCookieFromRootDSE();
+    searchChangelogUsingCookie("(targetDN=*)", cookie, 0, SUCCESS, test);
+
+    // search with an old cookie
+    searchOp = searchChangelogUsingCookie("(targetDN=*)", firstCookie, 0, UNWILLING_TO_PERFORM, test);
+    assertThat(searchOp.getErrorMessage().toString()).
+      contains("unknown replicated domain", TEST_ROOT_DN_STRING.toString());
+
+    debugInfo(test, "Ending test successfully");
+  }
+
+  /**
+   * This test enables a second suffix. It will break all tests using search on
+   * one suffix if run before them, so it is necessary to add them as
+   * dependencies.
+   */
+  @Test(enabled=true, dependsOnMethods = {
+    "searchInCookieModeOnOneSuffixUsingEmptyCookie",
+    "searchInCookieModeOnOneSuffix",
+    "searchInCookieModeAfterDomainIsRemoved",
+    "searchInDraftModeOnOneSuffixMultipleTimes",
+    "searchInDraftModeOnOneSuffix",
+    "searchInDraftModeWithInvalidChangeNumber" })
+  public void searchInCookieModeOnTwoSuffixes() throws Exception
+  {
+    String test = "CookieTwoSuffixes";
+    debugInfo(test, "Starting test\n\n");
+    Backend<?> backendForSecondSuffix = null;
+    try
+    {
+      backendForSecondSuffix = initializeMemoryBackend(true, TEST_BACKEND_ID2);
+
+      // publish 4 changes (2 on each suffix)
+      long time = TimeThread.getTime();
+      int seqNum = 1;
+      CSN csn1 = new CSN(time, seqNum++, SERVER_ID_1);
+      CSN csn2 = new CSN(time, seqNum++, SERVER_ID_2);
+      CSN csn3 = new CSN(time, seqNum++, SERVER_ID_2);
+      CSN csn4 = new CSN(time, seqNum++, SERVER_ID_1);
+
+      publishUpdateMessagesInOTest(test, false, generateDeleteMsg(TEST_ROOT_DN_STRING,  csn1, test, 1));
+
+      publishUpdateMessagesInOTest2(test, false,
+        generateDeleteMsg(TEST_ROOT_DN_STRING2,  csn2, test, 2),
+        generateDeleteMsg(TEST_ROOT_DN_STRING2,  csn3, test, 3));
+
+      publishUpdateMessagesInOTest(test, false, generateDeleteMsg(TEST_ROOT_DN_STRING,  csn4, test, 4));
+
+      // search on all suffixes using empty cookie
+      String cookie = "";
+      InternalSearchOperation searchOp =
+          searchChangelogUsingCookie("(targetDN=*" + test + "*)", cookie, 4, SUCCESS, test);
+      cookie = readCookieFromNthEntry(searchOp.getSearchEntries(), 2);
+
+      // search using previous cookie and expect to get ONLY the 4th change
+      LDIFWriter ldifWriter = getLDIFWriter();
+      searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*)", cookie, 1, SUCCESS, test);
+      cookie = assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn4);
+
+      // publish a new change on first suffix
+      CSN csn5 = new CSN(time, seqNum++, SERVER_ID_1);
+      publishUpdateMessagesInOTest(test, false, generateDeleteMsg(TEST_ROOT_DN_STRING,  csn5, test, 5));
+
+      // search using last cookie and expect to get the last change
+      searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*)", cookie, 1, SUCCESS, test);
+      assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn5);
+
+      // search on first suffix only, with empty cookie
+      cookie = "";
+      searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie, 3, SUCCESS, test);
+      cookie = assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter,
+          csn1, csn4, csn5);
+
+      // publish 4 more changes (2 on each suffix, on differents server id)
+      time = TimeThread.getTime();
+      seqNum = 6;
+      int serverId11 = 1203;
+      int serverId22 = 1204;
+      CSN csn6 = new CSN(time, seqNum++, serverId11);
+      CSN csn7 = new CSN(time, seqNum++, serverId22);
+      CSN csn8 = new CSN(time, seqNum++, serverId11);
+      CSN csn9 = new CSN(time, seqNum++, serverId22);
+
+      publishUpdateMessages(test, DN_OTEST2, serverId11, false,
+          generateDeleteMsg(TEST_ROOT_DN_STRING2,  csn6, test, 6));
+
+      publishUpdateMessages(test, DN_OTEST, serverId22, false,
+          generateDeleteMsg(TEST_ROOT_DN_STRING,  csn7, test, 7));
+
+      publishUpdateMessages(test, DN_OTEST2, serverId11, false,
+          generateDeleteMsg(TEST_ROOT_DN_STRING2,  csn8, test, 8));
+
+      publishUpdateMessages(test, DN_OTEST, serverId22, false,
+          generateDeleteMsg(TEST_ROOT_DN_STRING,  csn9, test, 9));
+
+      // ensure oldest state is correct for each suffix and for each server id
+      isOldestCSNForReplica(DN_OTEST, csn1);
+      isOldestCSNForReplica(DN_OTEST, csn7);
+
+      isOldestCSNForReplica(DN_OTEST2, csn2);
+      isOldestCSNForReplica(DN_OTEST2, csn6);
+
+      // test last cookie on root DSE
+      MultiDomainServerState expectedLastCookie =
+          new MultiDomainServerState("o=test:" + csn5 + " " + csn9 + ";o=test2:" + csn3 + " " + csn8 + ";");
+      final String lastCookie = readLastCookieFromRootDSE();
+      assertThat(lastCookie).isEqualTo(expectedLastCookie.toString());
+
+      // test unknown domain in provided cookie
+      // This case seems to be very hard to obtain in the real life
+      // (how to remove a domain from a RS topology ?)
+      final String cookie2 = lastCookie + "o=test6:";
+      debugInfo(test, "Search with bad domain in cookie=" + cookie);
+      searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie2, 0, UNWILLING_TO_PERFORM, test);
+      // the last cookie value may not match due to order of domain dn which is not guaranteed, so do not test it
+      String expectedError = ERR_RESYNC_REQUIRED_UNKNOWN_DOMAIN_IN_PROVIDED_COOKIE.get("[o=test6]", "")
+          .toString().replaceAll("<>", "");
+      assertThat(searchOp.getErrorMessage().toString()).startsWith(expectedError);
+
+      // test missing domain in provided cookie
+      final String cookie3 = lastCookie.substring(lastCookie.indexOf(';')+1);
+      debugInfo(test, "Search with bad domain in cookie=" + cookie);
+      searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie3, 0, UNWILLING_TO_PERFORM, test);
+      expectedError = ERR_RESYNC_REQUIRED_MISSING_DOMAIN_IN_PROVIDED_COOKIE
+          .get("o=test:;","<"+ cookie3 + "o=test:;>").toString();
+      assertThat(searchOp.getErrorMessage().toString()).isEqualToIgnoringCase(expectedError);
+    }
+    finally
+    {
+      removeBackend(backendForSecondSuffix);
+    }
+  }
+
+  private void isOldestCSNForReplica(DN baseDN, CSN csn) throws Exception
+  {
+    final ReplicationDomainDB domainDB = replicationServer.getChangelogDB().getReplicationDomainDB();
+    final DBCursor<UpdateMsg> cursor =
+        domainDB.getCursorFrom(baseDN, csn.getServerId(), csn, GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY);
+    try {
+      assertTrue(cursor.next(),
+          "Expected to be to find at least one change in replicaDB(" + baseDN + " " + csn.getServerId() + ")");
+      assertEquals(cursor.getRecord().getCSN(), csn);
+    }finally{
+      close(cursor);
+    }
+  }
+
+  @Test(enabled=true, dependsOnMethods = { "searchInCookieModeOnTwoSuffixes" })
+  public void searchInCookieModeOnTwoSuffixesWithPrivateBackend() throws Exception
+  {
+      String test = "CookiePrivateBackend";
+      debugInfo(test, "Starting test");
+
+      // Use o=test3 to avoid collision with o=test2 already used by a previous test
+      Backend<?> backend3 = null;
+      Pair<ReplicationBroker,LDAPReplicationDomain> replication1 = null;
+      LDAPReplicationDomain domain2 = null;
+      try {
+        replication1 = enableReplication(DN_OTEST, SERVER_ID_1, replicationServerPort, brokerSessionTimeout);
+
+        // create and publish 1 change on each suffix
+        long time = TimeThread.getTime();
+        CSN csn1 = new CSN(time, 1, SERVER_ID_1);
+        ReplicationBroker broker = replication1.getFirst();
+        broker.publish(generateDeleteMsg(TEST_ROOT_DN_STRING,  csn1, test, 1));
+
+        // create backend and configure replication for it
+        backend3 = initializeMemoryBackend(false, TEST_BACKEND_ID3);
+        backend3.setPrivateBackend(true);
+        DomainFakeCfg domainConf2 = new DomainFakeCfg(DN_OTEST3, 1602,
+            newSortedSet("localhost:" + replicationServerPort));
+        domain2 = startNewReplicationDomain(domainConf2, null, null);
+
+        // add a root entry to the backend
+        Thread.sleep(1000);
+        addEntry(createEntry(DN_OTEST3));
+
+        // expect entry from o=test2 to be returned
+        String cookie = "";
+        searchChangelogUsingCookie("(targetDN=*)", cookie, 2, SUCCESS, test);
+
+        ExternalChangelogDomainCfg eclCfg = new ExternalChangelogDomainFakeCfg(false, null, null);
+        domainConf2.setExternalChangelogDomain(eclCfg);
+        domain2.applyConfigurationChange(domainConf2);
+
+        // expect only entry from o=test returned
+        searchChangelogUsingCookie("(targetDN=*)", cookie, 1, SUCCESS, test);
+
+        // test the lastExternalChangelogCookie attribute of the ECL
+        // (does only refer to non private backend)
+        String expectedLastCookie = "o=test:" + csn1 + ";";
+        String lastCookie = readLastCookieFromRootDSE();
+        assertThat(expectedLastCookie.toString()).isEqualTo(lastCookie);
+      }
+      finally
+      {
+        removeReplicationDomains(replication1.getSecond(), domain2);
+        removeBackend(backend3);
+        stop(replication1.getFirst());
+      }
+      debugInfo(test, "Ending test successfully");
+  }
+
+  @Test
+  public void searchInDraftModeWithInvalidChangeNumber() throws Exception
+  {
+    String testName = "UnknownChangeNumber";
+    debugInfo(testName, "Starting test\n\n");
+
+    searchChangelog("(changenumber=1000)", 0, SUCCESS, testName);
+
+    debugInfo(testName, "Ending test with success");
+  }
+
+  @Test
+  public void searchInDraftModeOnOneSuffix() throws Exception
+  {
+    long firstChangeNumber = 1;
+    String testName = "FourChanges/" + firstChangeNumber;
+    debugInfo(testName, "Starting test\n\n");
+
+    CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(testName, false);
+
+    searchChangesForEachOperationTypeUsingDraftMode(firstChangeNumber, csns, testName);
+
+    assertChangelogAttributesInRootDSE(true, 1, 4);
+
+    debugInfo(testName, "Ending search with success");
+  }
+
+  @Test
+  public void searchInDraftModeOnOneSuffixMultipleTimes() throws Exception
+  {
+    replicationServer.getChangelogDB().setPurgeDelay(0);
+
+    // write 4 changes starting from changenumber 1, and search them
+    String testName = "Multiple/1";
+    CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(testName, false);
+    searchChangesForEachOperationTypeUsingDraftMode(1, csns, testName);
+
+    // write 4 more changes starting from changenumber 5, and search them
+    testName = "Multiple/5";
+    csns = generateAndPublishUpdateMsgForEachOperationType(testName, false);
+    searchChangesForEachOperationTypeUsingDraftMode(5, csns, testName);
+
+    // search from the provided change number: 6 (should be the add msg)
+    CSN csnOfLastAddMsg = csns[1];
+    searchChangelogForOneChangeNumber(6, csnOfLastAddMsg);
+
+    // search from a provided change number interval: 5-7
+    searchChangelogFromToChangeNumber(5,7);
+
+    // check first and last change number
+    assertChangelogAttributesInRootDSE(true, 1, 8);
+
+    // add a new change, then check again first and last change number without previous search
+    CSN csn = new CSN(TimeThread.getTime(), 10, SERVER_ID_1);
+    publishUpdateMessagesInOTest(testName, false, generateDeleteMsg(TEST_ROOT_DN_STRING, csn, testName, 1));
+
+    assertChangelogAttributesInRootDSE(true, 1, 9);
+  }
+
+  /**
+   * Verifies that is not possible to read the changelog without the changelog-read privilege
+   */
+  @Test
+  public void searchingWithoutPrivilegeShouldFail() throws Exception
+  {
+    AuthenticationInfo nonPrivilegedUser = new AuthenticationInfo();
+
+    InternalClientConnection conn = new InternalClientConnection(nonPrivilegedUser);
+    InternalSearchOperation op = conn.processSearch("cn=changelog", SearchScope.WHOLE_SUBTREE, "(objectclass=*)");
+
+    assertEquals(op.getResultCode(), ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
+    assertEquals(op.getErrorMessage().toMessage(), NOTE_SEARCH_CHANGELOG_INSUFFICIENT_PRIVILEGES.get());
+  }
+
+  @Test(enabled=true, dependsOnMethods = { "searchInCookieModeOnTwoSuffixesWithPrivateBackend"})
+  public void searchInCookieModeUseOfIncludeAttributes() throws Exception
+  {
+    String test = "IncludeAttributes";
+    debugInfo(test, "Starting test\n\n");
+
+    // Use o=test4 and o=test5 to avoid collision with existing suffixes already used by previous test
+    final String backendId4 = "test4";
+    final DN baseDN4 = DN.valueOf("o=" + backendId4);
+    final String backendId5 = "test5";
+    final DN baseDN5 = DN.valueOf("o=" + backendId5);
+    Backend<?> backend4 = null;
+    Backend<?> backend5 = null;
+    LDAPReplicationDomain domain4 = null;
+    LDAPReplicationDomain domain5 = null;
+    LDAPReplicationDomain domain41 = null;
+    try
+    {
+      SortedSet<String> replServers = newSortedSet("localhost:" + replicationServerPort);
+
+      // backend4 and domain4
+      backend4 = initializeMemoryBackend(false, backendId4);
+      DomainFakeCfg domainConf = new DomainFakeCfg(baseDN4, 1702, replServers);
+      SortedSet<String> eclInclude = newSortedSet("sn", "roomnumber");
+      domain4 = startNewReplicationDomain(domainConf, eclInclude, eclInclude);
+
+      // backend5 and domain5
+      backend5 = initializeMemoryBackend(false, backendId5);
+      domainConf = new DomainFakeCfg(baseDN5, 1703, replServers);
+      eclInclude = newSortedSet("objectclass");
+      SortedSet<String> eclIncludeForDeletes = newSortedSet("*");
+      domain5 = startNewReplicationDomain(domainConf, eclInclude, eclIncludeForDeletes);
+
+      // domain41
+      domainConf = new DomainFakeCfg(baseDN4, 1704, replServers);
+      eclInclude = newSortedSet("cn");
+      domain41 = startNewReplicationDomain(domainConf, eclInclude, eclInclude);
+
+      Thread.sleep(1000);
+
+      addEntry(createEntry(baseDN4));
+      addEntry(createEntry(baseDN5));
+
+      Entry uentry1 = entryFromLdifString(makeLdif(
+          "dn: cn=Fiona Jensen,o=" + backendId4,
+          "objectclass: top",
+          "objectclass: person",
+          "objectclass: organizationalPerson",
+          "objectclass: inetOrgPerson",
+          "cn: Fiona Jensen",
+          "sn: Jensen",
+          "uid: fiona",
+          "telephonenumber: 12121212"));
+      addEntry(uentry1);
+
+      Entry uentry2 = entryFromLdifString(makeLdif(
+          "dn: cn=Robert Hue,o=" + backendId5,
+          "objectclass: top",
+          "objectclass: person",
+          "objectclass: organizationalPerson",
+          "objectclass: inetOrgPerson",
+          "cn: Robert Hue",
+          "sn: Robby",
+          "uid: robert",
+          "telephonenumber: 131313"));
+      addEntry(uentry2);
+
+      // mod 'sn' of fiona with 'sn' configured as ecl-incl-att
+      final ModifyOperation modOp1 = connection.processModify(uentry1.getName(), createAttributeModif("sn", "newsn"));
+      waitForSearchOpResult(modOp1, ResultCode.SUCCESS);
+
+      // mod 'telephonenumber' of robert
+      final ModifyOperation modOp2 = connection.processModify(uentry2.getName(),
+          createAttributeModif("telephonenumber", "555555"));
+      waitForSearchOpResult(modOp2, ResultCode.SUCCESS);
+
+      // moddn robert to robert2
+      ModifyDNOperation modDNOp = connection.processModifyDN(
+          DN.valueOf("cn=Robert Hue," + baseDN5),
+          RDN.decode("cn=Robert Hue2"), true,
+          baseDN5);
+      waitForSearchOpResult(modDNOp, ResultCode.SUCCESS);
+
+      // del robert
+      final DeleteOperation delOp = connection.processDelete(DN.valueOf("cn=Robert Hue2," + baseDN5));
+      waitForSearchOpResult(delOp, ResultCode.SUCCESS);
+
+      // Search on all suffixes
+      String cookie = "";
+      InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*)", cookie, 8, SUCCESS, test);
+
+      for (SearchResultEntry resultEntry : searchOp.getSearchEntries())
+      {
+        String targetdn = getAttributeValue(resultEntry, "targetdn");
+
+        if (targetdn.endsWith("cn=robert hue,o=" + backendId5)
+            || targetdn.endsWith("cn=robert hue2,o="  + backendId5))
+        {
+          Entry targetEntry = parseIncludedAttributes(resultEntry, targetdn);
+
+          Set<String> eoc = newSet("person", "inetOrgPerson", "organizationalPerson", "top");
+          assertAttributeValues(targetEntry, "objectclass", eoc);
+
+          String changeType = getAttributeValue(resultEntry, "changetype");
+          if ("delete".equals(changeType))
+          {
+            // We are using "*" for deletes so should get back 4 attributes.
+            assertThat(targetEntry.getAttributes()).hasSize(4);
+            assertAttributeValue(targetEntry, "uid", "robert");
+            assertAttributeValue(targetEntry, "cn", "Robert Hue2");
+            assertAttributeValue(targetEntry, "telephonenumber", "555555");
+            assertAttributeValue(targetEntry, "sn", "Robby");
+          }
+          else
+          {
+            assertThat(targetEntry.getAttributes()).isEmpty();
+          }
+        }
+        else if (targetdn.endsWith("cn=fiona jensen,o=" + backendId4))
+        {
+          Entry targetEntry = parseIncludedAttributes(resultEntry, targetdn);
+
+          assertThat(targetEntry.getAttributes()).hasSize(2);
+          assertAttributeValue(targetEntry,"sn","jensen");
+          assertAttributeValue(targetEntry,"cn","Fiona Jensen");
+        }
+        assertAttributeValue(resultEntry,"changeinitiatorsname", "cn=Internal Client,cn=Root DNs,cn=config");
+      }
+    }
+    finally
+    {
+      final DN fionaDN = DN.valueOf("cn=Fiona Jensen,o=" + backendId4);
+      waitForSearchOpResult(connection.processDelete(fionaDN), ResultCode.SUCCESS);
+      waitForSearchOpResult(connection.processDelete(baseDN4), ResultCode.SUCCESS);
+      waitForSearchOpResult(connection.processDelete(baseDN5), ResultCode.SUCCESS);
+
+      removeReplicationDomains(domain41, domain4, domain5);
+      removeBackend(backend4, backend5);
+    }
+    debugInfo(test, "Ending test with success");
+  }
+
+  /**
+   * With an empty RS, a search should return only root entry.
+   */
+  @Test
+  public void searchWhenNoChangesShouldReturnRootEntryOnly() throws Exception
+  {
+    String testName = "EmptyRS";
+    debugInfo(testName, "Starting test\n\n");
+
+    searchChangelog("(objectclass=*)", 1, SUCCESS, testName);
+
+    debugInfo(testName, "Ending test successfully");
+  }
+
+  @Test
+  public void operationalAndVirtualAttributesShouldNotBeVisibleOutsideRootDSE() throws Exception
+  {
+    String testName = "attributesVisibleOutsideRootDSE";
+    debugInfo(testName, "Starting test \n\n");
+
+    Set<String> attributes =
+        newSet("firstchangenumber", "lastchangenumber", "changelog", "lastExternalChangelogCookie");
+
+    InternalSearchOperation searchOp = searchDNWithBaseScope(TEST_ROOT_DN_STRING, attributes);
+    waitForSearchOpResult(searchOp, ResultCode.SUCCESS);
+
+    final List<SearchResultEntry> entries = searchOp.getSearchEntries();
+    assertThat(entries).hasSize(1);
+    debugAndWriteEntries(null, entries, testName);
+    SearchResultEntry entry = entries.get(0);
+    assertNull(getAttributeValue(entry, "firstchangenumber"));
+    assertNull(getAttributeValue(entry, "lastchangenumber"));
+    assertNull(getAttributeValue(entry, "changelog"));
+    assertNull(getAttributeValue(entry, "lastExternalChangelogCookie"));
+
+    debugInfo(testName, "Ending test with success");
+  }
+
+  @DataProvider()
+  Object[][] getFilters()
+  {
+    return new Object[][] {
+      // base DN, filter, expected first change number, expected last change number
+      { "cn=changelog", "(objectclass=*)", -1, -1 },
+      { "cn=changelog", "(changenumber>=2)", 2, -1 },
+      { "cn=changelog", "(&(changenumber>=2)(changenumber<=5))", 2, 5 },
+      { "cn=changelog", "(&(dc=x)(&(changenumber>=2)(changenumber<=5)))", 2, 5 },
+      { "cn=changelog",
+          "(&(&(changenumber>=3)(changenumber<=4))(&(|(dc=y)(dc=x))(&(changenumber>=2)(changenumber<=5))))", 3, 4 },
+      { "cn=changelog", "(|(objectclass=*)(&(changenumber>=2)(changenumber<=5)))", -1, -1 },
+      { "cn=changelog", "(changenumber=8)", 8, 8 },
+
+      { "changeNumber=8,cn=changelog", "(objectclass=*)", 8, 8 },
+      { "changeNumber=8,cn=changelog", "(changenumber>=2)", 8, 8 },
+      { "changeNumber=8,cn=changelog", "(&(changenumber>=2)(changenumber<=5))", 8, 8 },
+    };
+  }
+
+  @Test(dataProvider="getFilters")
+  public void optimizeFiltersWithChangeNumber(String dn, String filter, long expectedFirstCN, long expectedLastCN)
+      throws Exception
+  {
+    final ChangelogBackend backend = new ChangelogBackend(null, null);
+    final DN baseDN = DN.valueOf(dn);
+    final SearchParams searchParams = new SearchParams();
+
+    backend.optimizeSearchParameters(searchParams, baseDN, SearchFilter.createFilterFromString(filter));
+
+    assertSearchParameters(searchParams, expectedFirstCN, expectedLastCN, null);
+  }
+
+  @Test
+  public void optimizeFiltersWithReplicationCsn() throws Exception
+  {
+    final ChangelogBackend backend = new ChangelogBackend(null, null);
+    final DN baseDN = DN.valueOf("cn=changelog");
+    final CSN csn = new CSNGenerator(1, 0).newCSN();
+    final SearchParams searchParams = new SearchParams();
+
+    backend.optimizeSearchParameters(searchParams, baseDN,
+        SearchFilter.createFilterFromString("(replicationcsn=" + csn + ")"));
+
+    assertSearchParameters(searchParams, -1, -1, csn);
+  }
+
+  private List<SearchResultEntry> assertChangelogAttributesInRootDSE(boolean isECLEnabled,
+      int expectedFirstChangeNumber, int expectedLastChangeNumber) throws Exception
+  {
+    AssertionError error = null;
+    for (int count = 0 ; count < 30; count++)
+    {
+      try
+      {
+        final Set<String> attributes = new LinkedHashSet<String>();
+        if (expectedFirstChangeNumber > 0)
+        {
+          attributes.add("firstchangenumber");
+        }
+        attributes.add("lastchangenumber");
+        attributes.add("changelog");
+        attributes.add("lastExternalChangelogCookie");
+
+        final InternalSearchOperation searchOp = searchDNWithBaseScope("", attributes);
+        final List<SearchResultEntry> entries = searchOp.getSearchEntries();
+        assertThat(entries).hasSize(1);
+
+        final SearchResultEntry entry = entries.get(0);
+        if (isECLEnabled)
+        {
+          if (expectedFirstChangeNumber > 0)
+          {
+            assertAttributeValue(entry, "firstchangenumber", String.valueOf(expectedFirstChangeNumber));
+          }
+          assertAttributeValue(entry, "lastchangenumber", String.valueOf(expectedLastChangeNumber));
+          assertAttributeValue(entry, "changelog", String.valueOf("cn=changelog"));
+          assertNotNull(getAttributeValue(entry, "lastExternalChangelogCookie"));
+        }
+        else
+        {
+          if (expectedFirstChangeNumber > 0) {
+            assertNull(getAttributeValue(entry, "firstchangenumber"));
+          }
+          assertNull(getAttributeValue(entry, "lastchangenumber"));
+          assertNull(getAttributeValue(entry, "changelog"));
+          assertNull(getAttributeValue(entry, "lastExternalChangelogCookie"));
+        }
+        return entries;
+      }
+      catch (AssertionError ae)
+      {
+        // try again to see if changes have been persisted
+        error = ae;
+      }
+      Thread.sleep(100);
+    }
+    assertNotNull(error);
+    throw error;
+  }
+
+  private String readLastCookieFromRootDSE() throws Exception
+  {
+    String cookie = "";
+    LDIFWriter ldifWriter = getLDIFWriter();
+
+    InternalSearchOperation searchOp = searchDNWithBaseScope("", newSet("lastExternalChangelogCookie"));
+    List<SearchResultEntry> entries = searchOp.getSearchEntries();
+    if (entries != null)
+    {
+      for (SearchResultEntry resultEntry : entries)
+      {
+        ldifWriter.writeEntry(resultEntry);
+        cookie = getAttributeValue(resultEntry, "lastexternalchangelogcookie");
+      }
+    }
+    return cookie;
+  }
+
+  private String assertLastCookieDifferentThanLastValue(final String lastCookie) throws Exception
+  {
+    int count = 0;
+    while (count < 100)
+    {
+      final String newCookie = readLastCookieFromRootDSE();
+      if (!newCookie.equals(lastCookie))
+      {
+        return newCookie;
+      }
+      count++;
+      Thread.sleep(10);
+    }
+    Assertions.fail("Expected last cookie should have been updated, but it always stayed at value '" + lastCookie + "'");
+    return null;// dead code
+  }
+
+  private String readCookieFromNthEntry(List<SearchResultEntry> entries, int i)
+  {
+    SearchResultEntry entry = entries.get(i);
+    return entry.getAttribute("changelogcookie").get(0).iterator().next().toString();
+  }
+
+  private String assertEntriesContainsCSNsAndReadLastCookie(String test, List<SearchResultEntry> entries,
+      LDIFWriter ldifWriter, CSN... csns) throws Exception
+  {
+    assertThat(getCSNsFromEntries(entries)).containsExactly(csns);
+    debugAndWriteEntries(ldifWriter, entries, test);
+    return readCookieFromNthEntry(entries, csns.length - 1);
+  }
+
+  private List<CSN> getCSNsFromEntries(List<SearchResultEntry> entries)
+  {
+    List<CSN> results = new ArrayList<CSN>(entries.size());
+    for (SearchResultEntry entry : entries)
+    {
+      results.add(new CSN(getAttributeValue(entry, "replicationCSN")));
+    }
+    return results;
+  }
+
+  private void assertSearchParameters(SearchParams searchParams, long firstChangeNumber,
+      long lastChangeNumber, CSN csn) throws Exception
+  {
+    assertEquals(searchParams.getLowestChangeNumber(), firstChangeNumber);
+    assertEquals(searchParams.getHighestChangeNumber(), lastChangeNumber);
+    assertEquals(searchParams.getCSN(), csn == null ? new CSN(0, 0, 0) : csn);
+  }
+
+  private CSN[] generateAndPublishUpdateMsgForEachOperationType(String testName, boolean checkLastCookie)
+      throws Exception
+  {
+    CSN[] csns = generateCSNs(4, SERVER_ID_1);
+    publishUpdateMessagesInOTest(testName, checkLastCookie,
+        generateDeleteMsg(TEST_ROOT_DN_STRING, csns[0], testName, 1),
+        generateAddMsg(TEST_ROOT_DN_STRING, csns[1], USER1_ENTRY_UUID, testName),
+        generateModMsg(TEST_ROOT_DN_STRING, csns[2], testName),
+        generateModDNMsg(TEST_ROOT_DN_STRING, csns[3], testName));
+    return csns;
+  }
+
+  // shortcut method for default base DN and server id used in tests
+  private void publishUpdateMessagesInOTest(String testName, boolean checkLastCookie, UpdateMsg...messages)
+      throws Exception
+  {
+    publishUpdateMessages(testName, DN_OTEST, SERVER_ID_1, checkLastCookie, messages);
+  }
+
+  private void publishUpdateMessagesInOTest2(String testName, boolean checkLastCookie, UpdateMsg...messages)
+      throws Exception
+  {
+    publishUpdateMessages(testName, DN_OTEST2, SERVER_ID_2, checkLastCookie, messages);
+  }
+
+  /**
+   * Publish a list of update messages to the replication broker corresponding to given baseDN and server id.
+   *
+   * @param checkLastCookie if true, checks that last cookie is update after each message publication
+   */
+  private void publishUpdateMessages(String testName, DN baseDN, int serverId, boolean checkLastCookie,
+      ReplicationMsg...messages) throws Exception
+  {
+    Pair<ReplicationBroker, LDAPReplicationDomain> replicationObjects = null;
+    try
+    {
+      replicationObjects = enableReplication(baseDN, serverId, replicationServerPort, brokerSessionTimeout);
+      ReplicationBroker broker = replicationObjects.getFirst();
+      String cookie = "";
+      for (ReplicationMsg msg : messages)
+      {
+        if (msg instanceof UpdateMsg)
+        {
+          debugInfo(testName, " publishes " + ((UpdateMsg)msg).getCSN());
+        }
+
+        broker.publish(msg);
+
+        if (checkLastCookie)
+        {
+          cookie = assertLastCookieDifferentThanLastValue(cookie);
+        }
+      }
+    }
+    finally
+    {
+      if (replicationObjects != null)
+      {
+        removeReplicationDomains(replicationObjects.getSecond());
+        stop(replicationObjects.getFirst());
+      }
+    }
+  }
+
+  private String[] buildCookiesFromCsns(CSN[] csns)
+  {
+    final String[] cookies = new String[csns.length];
+    for (int j = 0; j < cookies.length; j++)
+    {
+      cookies[j] = "o=test:" + csns[j] + ";";
+    }
+    return cookies;
+  }
+
+  private void searchChangesForEachOperationTypeUsingDraftMode(long firstChangeNumber, CSN[] csns, String testName)
+      throws Exception
+  {
+    // Search the changelog and check 4 entries are returned
+    String filter = "(targetdn=*" + testName + "*,o=test)";
+    InternalSearchOperation searchOp = searchChangelog(filter, 4, SUCCESS, testName);
+
+    assertContainsNoControl(searchOp);
+    assertEntriesForEachOperationType(searchOp.getSearchEntries(), firstChangeNumber, testName, USER1_ENTRY_UUID, csns);
+
+    // Search the changelog with filter on change number and check 4 entries are returned
+    filter =
+        "(&(targetdn=*" + testName + "*,o=test)"
+          + "(&(changenumber>=" + firstChangeNumber + ")"
+            + "(changenumber<=" + (firstChangeNumber + 3) + ")))";
+    searchOp = searchChangelog(filter, 4, SUCCESS, testName);
+
+    assertContainsNoControl(searchOp);
+    assertEntriesForEachOperationType(searchOp.getSearchEntries(), firstChangeNumber, testName, USER1_ENTRY_UUID, csns);
+  }
+
+  /**
+   * Search on the provided change number and check the result.
+   *
+   * @param changeNumber
+   *          Change number to search
+   * @param expectedCsn
+   *          Expected CSN in the entry corresponding to the change number
+   */
+  private void searchChangelogForOneChangeNumber(long changeNumber, CSN expectedCsn) throws Exception
+  {
+    String testName = "searchOneChangeNumber/" + changeNumber;
+    debugInfo(testName, "Starting search\n\n");
+
+    InternalSearchOperation searchOp =
+        searchChangelog("(changenumber=" + changeNumber + ")", 1, SUCCESS, testName);
+
+    SearchResultEntry entry = searchOp.getSearchEntries().get(0);
+    String uncheckedUid = null;
+    assertEntryCommonAttributes(entry, uncheckedUid, USER1_ENTRY_UUID, changeNumber, expectedCsn,
+        "o=test:" + expectedCsn + ";");
+
+    debugInfo(testName, "Ending search with success");
+  }
+
+  private void searchChangelogFromToChangeNumber(int firstChangeNumber, int lastChangeNumber) throws Exception
+  {
+    String testName = "searchFromToChangeNumber/" + firstChangeNumber + "/" + lastChangeNumber;
+    debugInfo(testName, "Starting search\n\n");
+
+    String filter = "(&(changenumber>=" + firstChangeNumber + ")" + "(changenumber<=" + lastChangeNumber + "))";
+    final int expectedNbEntries = lastChangeNumber - firstChangeNumber + 1;
+    searchChangelog(filter, expectedNbEntries, SUCCESS, testName);
+
+    debugInfo(testName, "Ending search with success");
+  }
+
+  private InternalSearchOperation searchChangelogUsingCookie(String filterString,
+      String cookie, int expectedNbEntries, ResultCode expectedResultCode, String testName)
+      throws Exception
+  {
+    debugInfo(testName, "Search with cookie=[" + cookie + "] filter=[" + filterString + "]");
+    return searchChangelog(filterString, ALL_ATTRIBUTES, createCookieControl(cookie),
+        expectedNbEntries, expectedResultCode, testName);
+  }
+
+  private InternalSearchOperation searchChangelog(String filterString, int expectedNbEntries,
+      ResultCode expectedResultCode, String testName) throws Exception
+  {
+    return searchChangelog(filterString, ALL_ATTRIBUTES, NO_CONTROL, expectedNbEntries, expectedResultCode, testName);
+  }
+
+  private InternalSearchOperation searchChangelog(String filterString, Set<String> attributes,
+      List<Control> controls, int expectedNbEntries, ResultCode expectedResultCode, String testName) throws Exception
+  {
+    InternalSearchOperation searchOperation = null;
+    int sizeLimitZero = 0;
+    int timeLimitZero = 0;
+    InternalSearchListener noSearchListener = null;
+    int count = 0;
+    do
+    {
+      Thread.sleep(10);
+      boolean typesOnlyFalse = false;
+      searchOperation = connection.processSearch("cn=changelog", SearchScope.WHOLE_SUBTREE,
+          DereferenceAliasesPolicy.NEVER, sizeLimitZero, timeLimitZero, typesOnlyFalse, filterString,
+          attributes, controls, noSearchListener);
+      count++;
+    }
+    while (count < 300 && searchOperation.getSearchEntries().size() != expectedNbEntries);
+
+    final List<SearchResultEntry> entries = searchOperation.getSearchEntries();
+    assertThat(entries).hasSize(expectedNbEntries);
+    debugAndWriteEntries(getLDIFWriter(), entries, testName);
+    waitForSearchOpResult(searchOperation, expectedResultCode);
+    return searchOperation;
+  }
+
+  private InternalSearchOperation searchDNWithBaseScope(String dn, Set<String> attributes) throws Exception
+  {
+    final InternalSearchOperation searchOp = connection.processSearch(
+        dn,
+        SearchScope.BASE_OBJECT,
+        DereferenceAliasesPolicy.NEVER,
+        0,     // Size limit
+        0,     // Time limit
+        false, // Types only
+        "(objectclass=*)",
+        attributes);
+    waitForSearchOpResult(searchOp, ResultCode.SUCCESS);
+    return searchOp;
+  }
+
+  /** Build a list of controls including the cookie provided. */
+  private List<Control> createCookieControl(String cookie) throws DirectoryException
+  {
+    final MultiDomainServerState state = new MultiDomainServerState(cookie);
+    final Control cookieControl = new ExternalChangelogRequestControl(true, state);
+    return newList(cookieControl);
+  }
+
+  private static LDIFWriter getLDIFWriter() throws Exception
+  {
+    ByteArrayOutputStream stream = new ByteArrayOutputStream();
+    LDIFExportConfig exportConfig = new LDIFExportConfig(stream);
+    return new LDIFWriter(exportConfig);
+  }
+
+  private CSN[] generateCSNs(int numberOfCsns, int serverId)
+  {
+    long startTime = TimeThread.getTime();
+
+    CSN[] csns = new CSN[numberOfCsns];
+    for (int i = 0; i < numberOfCsns; i++)
+    {
+      // seqNum must be greater than 0, so start at 1
+      csns[i] = new CSN(startTime + i, i + 1, serverId);
+    }
+    return csns;
+  }
+
+  private UpdateMsg generateDeleteMsg(String baseDn, CSN csn, String testName, int testIndex)
+      throws Exception
+  {
+    String dn = "uid=" + testName + testIndex + "," + baseDn;
+    return new DeleteMsg(DN.valueOf(dn), csn, testName + "uuid" + testIndex);
+  }
+
+  private UpdateMsg generateAddMsg(String baseDn, CSN csn, String user1entryUUID, String testName)
+      throws Exception
+  {
+    String baseUUID = "22222222-2222-2222-2222-222222222222";
+    String entryLdif = "dn: uid="+ testName + "2," + baseDn + "\n"
+        + "objectClass: top\n" + "objectClass: domain\n"
+        + "entryUUID: "+ user1entryUUID +"\n";
+    Entry entry = TestCaseUtils.entryFromLdifString(entryLdif);
+    return new AddMsg(
+        csn,
+        DN.valueOf("uid="+testName+"2," + baseDn),
+        user1entryUUID,
+        baseUUID,
+        entry.getObjectClassAttribute(),
+        entry.getAttributes(),
+        Collections.<Attribute> emptyList());
+  }
+
+  private UpdateMsg generateModMsg(String baseDn, CSN csn, String testName) throws Exception
+  {
+    DN baseDN = DN.valueOf("uid=" + testName + "3," + baseDn);
+    List<Modification> mods = createAttributeModif("description", "new value");
+    return new ModifyMsg(csn, baseDN, mods, testName + "uuid3");
+  }
+
+  private List<Modification> createAttributeModif(String attributeName, String valueString)
+  {
+    Attribute attr = Attributes.create(attributeName, valueString);
+    return newArrayList(new Modification(ModificationType.REPLACE, attr));
+  }
+
+  private UpdateMsg generateModDNMsg(String baseDn, CSN csn, String testName) throws Exception
+  {
+    final DN newSuperior = DN_OTEST2;
+    ModifyDNOperation op = new ModifyDNOperationBasis(connection, 1, 1, null,
+        DN.valueOf("uid=" + testName + "4," + baseDn), // entryDN
+        RDN.decode("uid=" + testName + "new4"), // new rdn
+        true,  // deleteoldrdn
+        newSuperior);
+    op.setAttachment(SYNCHROCONTEXT, new ModifyDnContext(csn, testName + "uuid4", "newparentId"));
+    LocalBackendModifyDNOperation localOp = new LocalBackendModifyDNOperation(op);
+    return new ModifyDNMsg(localOp);
+  }
+
+  //TODO : share this code with other classes ?
+  private void waitForSearchOpResult(Operation operation, ResultCode expectedResult) throws Exception
+  {
+    int i = 0;
+    while (operation.getResultCode() == ResultCode.UNDEFINED || operation.getResultCode() != expectedResult)
+    {
+      Thread.sleep(50);
+      i++;
+      if (i > 10)
+      {
+        assertEquals(operation.getResultCode(), expectedResult, operation.getErrorMessage().toString());
+      }
+    }
+  }
+
+  /** Verify that no entry contains the ChangeLogCookie control. */
+  private void assertContainsNoControl(InternalSearchOperation searchOp)
+  {
+    for (SearchResultEntry entry : searchOp.getSearchEntries())
+    {
+      assertTrue(entry.getControls().isEmpty(), "result entry " + entry.toString() +
+          " should contain no control(s)");
+    }
+  }
+
+  /** Verify that all entries contains the ChangeLogCookie control with the correct cookie value. */
+  private void assertResultsContainCookieControl(InternalSearchOperation searchOp, String[] cookies) throws Exception
+  {
+    for (SearchResultEntry entry : searchOp.getSearchEntries())
+    {
+      boolean cookieControlFound = false;
+      for (Control control : entry.getControls())
+      {
+        if (control.getOID().equals(OID_ECL_COOKIE_EXCHANGE_CONTROL))
+        {
+          String cookieString =
+              searchOp.getRequestControl(ExternalChangelogRequestControl.DECODER).getCookie().toString();
+          assertThat(cookieString).isIn((Object[]) cookies);
+          cookieControlFound = true;
+        }
+      }
+      assertTrue(cookieControlFound, "result entry " + entry.toString() + " should contain the cookie control");
+    }
+  }
+
+  /** Check the DEL entry has the right content. */
+  private void assertDelEntry(SearchResultEntry entry, String uid, String entryUUID,
+      long changeNumber, CSN csn, String cookie)
+  {
+    assertAttributeValue(entry, "changetype", "delete");
+    assertAttributeValue(entry, "targetuniqueid", entryUUID);
+    assertAttributeValue(entry, "targetentryuuid", entryUUID);
+    assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn, cookie);
+  }
+
+  /** Check the ADD entry has the right content. */
+  private void assertAddEntry(SearchResultEntry entry, String uid, String entryUUID,
+      long changeNumber, CSN csn, String cookie)
+  {
+    assertAttributeValue(entry, "changetype", "add");
+    assertEntryMatchesLDIF(entry, "changes",
+        "objectClass: domain",
+        "objectClass: top",
+        "entryUUID: " + entryUUID);
+    assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn, cookie);
+  }
+
+  private void assertModEntry(SearchResultEntry entry, String uid, String entryUUID,
+      long changeNumber, CSN csn, String cookie)
+  {
+    assertAttributeValue(entry, "changetype", "modify");
+    assertEntryMatchesLDIF(entry, "changes",
+        "replace: description",
+        "description: new value",
+        "-");
+    assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn, cookie);
+  }
+
+  private void assertModDNEntry(SearchResultEntry entry, String uid, String newUid,
+      String entryUUID, long changeNumber, CSN csn, String cookie)
+  {
+    assertAttributeValue(entry, "changetype", "modrdn");
+    assertAttributeValue(entry, "newrdn", "uid=" + newUid);
+    assertAttributeValue(entry, "newsuperior", TEST_ROOT_DN_STRING2);
+    assertAttributeValue(entry, "deleteoldrdn", "true");
+    assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn, cookie);
+
+  }
+
+  private void assertEntryCommonAttributes(SearchResultEntry resultEntry,
+      String uid, String entryUUID, long changeNumber, CSN csn, String cookie)
+  {
+    if (changeNumber == 0)
+    {
+      assertDNWithCSN(resultEntry, csn);
+    }
+    else
+    {
+      assertDNWithChangeNumber(resultEntry, changeNumber);
+      assertAttributeValue(resultEntry, "changenumber", String.valueOf(changeNumber));
+    }
+    assertAttributeValue(resultEntry, "targetentryuuid", entryUUID);
+    assertAttributeValue(resultEntry, "replicaidentifier", String.valueOf(SERVER_ID_1));
+    assertAttributeValue(resultEntry, "replicationcsn", csn.toString());
+    assertAttributeValue(resultEntry, "changelogcookie", cookie);
+    // A null value can be provided for uid if it should not be checked
+    if (uid != null)
+    {
+      final String targetDN = "uid=" + uid + "," + TEST_ROOT_DN_STRING;
+      assertAttributeValue(resultEntry, "targetdn", targetDN);
+    }
+  }
+
+  private void assertEntriesForEachOperationType(List<SearchResultEntry> entries, long firstChangeNumber,
+      String testName, String entryUUID, CSN... csns) throws Exception
+  {
+    debugAndWriteEntries(getLDIFWriter(), entries, testName);
+
+    assertThat(entries).hasSize(4);
+
+    CSN csn = csns[0];
+    assertDelEntry(entries.get(0), testName + "1", testName + "uuid1", firstChangeNumber, csn, "o=test:" + csn + ";");
+
+    csn = csns[1];
+    assertAddEntry(entries.get(1), testName + "2", entryUUID, firstChangeNumber+1, csn, "o=test:" + csn + ";");
+
+    csn = csns[2];
+    assertModEntry(entries.get(2), testName + "3", testName + "uuid3", firstChangeNumber+2, csn,
+        "o=test:" + csn + ";");
+
+    csn = csns[3];
+    assertModDNEntry(entries.get(3), testName + "4", testName + "new4", testName + "uuid4", firstChangeNumber+3, csn,
+        "o=test:" + csn + ";");
+  }
+
+  /**
+   * Asserts the attribute value as LDIF to ignore lines ordering.
+   */
+  private static void assertEntryMatchesLDIF(Entry entry, String attrName, String... expectedLDIFLines)
+  {
+    final String actualVal = getAttributeValue(entry, attrName);
+    final Set<Set<String>> actual = toLDIFEntries(actualVal.split("\n"));
+    final Set<Set<String>> expected = toLDIFEntries(expectedLDIFLines);
+    assertThat(actual)
+        .as("In entry " + entry + " incorrect value for attr '" + attrName + "'")
+        .isEqualTo(expected);
+  }
+
+  private static void assertAttributeValues(Entry entry, String attrName, Set<String> expectedValues)
+  {
+    final Set<String> values = new HashSet<String>();
+    for (Attribute attr : entry.getAttribute(attrName))
+    {
+      for (ByteString value : attr)
+      {
+        values.add(value.toString());
+      }
+    }
+    assertThat(values)
+      .as("In entry " + entry + " incorrect values for attribute '" + attrName + "'")
+      .isEqualTo(expectedValues);
+  }
+
+  private static void assertAttributeValue(Entry entry, String attrName, String expectedValue)
+  {
+    assertFalse(expectedValue.contains("\n"),
+        "You should use assertEntryMatchesLDIF() method for asserting on this value: \"" + expectedValue + "\"");
+    final String actualValue = getAttributeValue(entry, attrName);
+    assertThat(actualValue)
+        .as("In entry " + entry + " incorrect value for attr '" + attrName + "'")
+        .isEqualToIgnoringCase(expectedValue);
+  }
+
+  private void assertDNWithChangeNumber(SearchResultEntry resultEntry, long changeNumber)
+  {
+    String actualDN = resultEntry.getName().toNormalizedString();
+    String expectedDN = "changenumber=" + changeNumber + ",cn=changelog";
+    assertThat(actualDN).isEqualToIgnoringCase(expectedDN);
+  }
+
+  private void assertDNWithCSN(SearchResultEntry resultEntry, CSN csn)
+  {
+    String actualDN = resultEntry.getName().toNormalizedString();
+    String expectedDN = "replicationcsn=" + csn + "," + TEST_ROOT_DN_STRING + ",cn=changelog";
+    assertThat(actualDN).isEqualToIgnoringCase(expectedDN);
+  }
+
+  /**
+   * Returns a data structure allowing to compare arbitrary LDIF lines. The
+   * algorithm splits LDIF entries on lines containing only a dash ("-"). It
+   * then returns LDIF entries and lines in an LDIF entry in ordering
+   * insensitive data structures.
+   * <p>
+   * Note: a last line with only a dash ("-") is significant. i.e.:
+   *
+   * <pre>
+   * <code>
+   * boolean b = toLDIFEntries("-").equals(toLDIFEntries()));
+   * System.out.println(b); // prints "false"
+   * </code>
+   * </pre>
+   */
+  private static Set<Set<String>> toLDIFEntries(String... ldifLines)
+  {
+    final Set<Set<String>> results = new HashSet<Set<String>>();
+    Set<String> ldifEntryLines = new HashSet<String>();
+    for (String ldifLine : ldifLines)
+    {
+      if (!"-".equals(ldifLine))
+      {
+        // same entry keep adding
+        ldifEntryLines.add(ldifLine);
+      }
+      else
+      {
+        // this is a new entry
+        results.add(ldifEntryLines);
+        ldifEntryLines = new HashSet<String>();
+      }
+    }
+    results.add(ldifEntryLines);
+    return results;
+  }
+
+  private static String getAttributeValue(Entry entry, String attrName)
+  {
+    List<Attribute> attrs = entry.getAttribute(attrName.toLowerCase());
+    if (attrs == null)
+    {
+      return null;
+    }
+    Attribute attr = attrs.iterator().next();
+    ByteString value = attr.iterator().next();
+    return value.toString();
+  }
+
+  private Entry parseIncludedAttributes(SearchResultEntry resultEntry, String targetdn) throws Exception
+  {
+    // Parse includedAttributes as an entry.
+    String includedAttributes = getAttributeValue(resultEntry, "includedattributes");
+    String[] ldifAttributeLines = includedAttributes.split("\\n");
+    String[] ldif = new String[ldifAttributeLines.length + 1];
+    System.arraycopy(ldifAttributeLines, 0, ldif, 1, ldifAttributeLines.length);
+    ldif[0] = "dn: " + targetdn;
+    return TestCaseUtils.makeEntry(ldif);
+  }
+
+  private void debugAndWriteEntries(LDIFWriter ldifWriter,List<SearchResultEntry> entries, String tn) throws Exception
+  {
+    if (entries != null)
+    {
+      for (SearchResultEntry entry : entries)
+      {
+        // Can use entry.toSingleLineString()
+        debugInfo(tn, " RESULT entry returned:" + entry.toLDIFString());
+        if (ldifWriter != null)
+        {
+          ldifWriter.writeEntry(entry);
+        }
+      }
+    }
+  }
+
+  /**
+   * Creates a memory backend, to be used as additional backend in tests.
+   */
+  private static Backend<?> initializeMemoryBackend(boolean createBaseEntry, String backendId) throws Exception
+  {
+    DN baseDN = DN.valueOf("o=" + backendId);
+
+    //  Retrieve backend. Warning: it is important to perform this each time,
+    //  because a test may have disabled then enabled the backend (i.e a test
+    //  performing an import task). As it is a memory backend, when the backend
+    //  is re-enabled, a new backend object is in fact created and old reference
+    //  to memory backend must be invalidated. So to prevent this problem, we
+    //  retrieve the memory backend reference each time before cleaning it.
+    MemoryBackend memoryBackend = (MemoryBackend) DirectoryServer.getBackend(backendId);
+
+    if (memoryBackend == null)
+    {
+      memoryBackend = new MemoryBackend();
+      memoryBackend.setBackendID(backendId);
+      memoryBackend.setBaseDNs(new DN[] {baseDN});
+      memoryBackend.initializeBackend();
+      DirectoryServer.registerBackend(memoryBackend);
+    }
+
+    memoryBackend.clearMemoryBackend();
+
+    if (createBaseEntry)
+    {
+      memoryBackend.addEntry(createEntry(baseDN), null);
+    }
+    return memoryBackend;
+  }
+
+  private static void removeBackend(Backend<?>... backends)
+  {
+    for (Backend<?> backend : backends)
+    {
+      if (backend != null)
+      {
+        MemoryBackend memoryBackend = (MemoryBackend) backend;
+        memoryBackend.clearMemoryBackend();
+        memoryBackend.finalizeBackend();
+        DirectoryServer.deregisterBackend(memoryBackend);
+      }
+    }
+  }
+
+  /**
+   * Utility - log debug message - highlight it is from the test and not
+   * from the server code. Makes easier to observe the test steps.
+   */
+  private void debugInfo(String testName, String message)
+  {
+    logger.trace("** TEST %s ** %s", testName, message);
+  }
+}
+

--
Gitblit v1.10.0