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