/* * 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-2015 ForgeRock AS. */ package org.opends.server.backends; import static java.util.concurrent.TimeUnit.*; 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.opends.server.util.TestTimer.*; import static org.testng.Assert.*; import java.io.ByteArrayOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.concurrent.Callable; import org.assertj.core.api.SoftAssertions; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ModificationType; import org.forgerock.opendj.ldap.ResultCode; import org.forgerock.opendj.ldap.SearchScope; import org.opends.server.admin.std.server.ExternalChangelogDomainCfg; import org.opends.server.api.Backend; import org.opends.server.backends.ChangelogBackend.ChangeNumberRange; import org.opends.server.controls.EntryChangelogNotificationControl; 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.InternalSearchOperation; import org.opends.server.protocols.internal.Requests; import org.opends.server.protocols.internal.SearchRequest; 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.DBCursor.CursorOptions; import org.opends.server.replication.server.changelog.api.ReplicaId; import org.opends.server.replication.server.changelog.api.ReplicationDomainDB; import org.opends.server.replication.server.changelog.file.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.TestTimer; 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; @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 static ReplicaId server1; private static ReplicaId server2; 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; private final List<LDAPReplicationDomain> domains = new ArrayList<>(); private final Map<ReplicaId, ReplicationBroker> brokers = new HashMap<>(); @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); server1 = ReplicaId.of(DN_OTEST, SERVER_ID_1); server2 = ReplicaId.of(DN_OTEST2, SERVER_ID_2); // 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 { removeReplicationDomains(domains.toArray(new LDAPReplicationDomain[domains.size()])); domains.clear(); stop(brokers.values().toArray(new ReplicationBroker[brokers.size()])); brokers.clear(); clearChangelogDB(replicationServer); } /** Configure a replicationServer for test. */ private void configureReplicationServer() throws Exception { replicationServerPort = findFreePort(); ReplServerFakeConfiguration config = new ReplServerFakeConfiguration( replicationServerPort, "ChangelogBackendTestDB", 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 ReplicationBroker enableReplication(ReplicaId replicaId) throws Exception { ReplicationBroker broker = brokers.get(replicaId); if (broker == null) { broker = openReplicationSession(replicaId.getBaseDN(), replicaId.getServerId(), 100, replicationServerPort, 5000); brokers.put(replicaId, broker); DomainFakeCfg domainConf = newFakeCfg(replicaId.getBaseDN(), replicaId.getServerId(), replicationServerPort); startNewReplicationDomain(domainConf, null, null); } return broker; } /** Start a new replication domain on the directory server side. */ private LDAPReplicationDomain startNewReplicationDomain( DomainFakeCfg domainConf, SortedSet<String> eclInclude, SortedSet<String> eclIncludeForDeletes) throws Exception { LDAPReplicationDomain domain = MultimasterReplication.findDomain(domainConf.getBaseDN(), null); if (domain == null) { 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); domain = MultimasterReplication.createNewDomain(domainConf); domain.start(); domains.add(domain); } return domain; } 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); int nbEntries = 4; String cookie = ""; InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookie, SUCCESS, nbEntries, test); final List<SearchResultEntry> searchEntries = searchOp.getSearchEntries(); assertDelEntry(searchEntries.get(0), test + 1, test + "uuid1", CHANGENUMBER_ZERO, csns[0]); assertAddEntry(searchEntries.get(1), test + 2, USER1_ENTRY_UUID, CHANGENUMBER_ZERO, csns[1]); assertModEntry(searchEntries.get(2), test + 3, test + "uuid3", CHANGENUMBER_ZERO, csns[2]); assertModDNEntry(searchEntries.get(3), test + 4, test + "new4", test + "uuid4", CHANGENUMBER_ZERO, csns[3]); assertResultsContainCookieControl(searchOp, newArrayList(buildCookiesFromCsns(csns))); assertChangelogAttributesInRootDSE(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], SUCCESS, nbEntries, test); List<SearchResultEntry> searchEntries = searchOp.getSearchEntries(); assertAddEntry(searchEntries.get(0), test + 2, USER1_ENTRY_UUID, CHANGENUMBER_ZERO, csns[1]); assertModEntry(searchEntries.get(1), test + 3, test + "uuid3", CHANGENUMBER_ZERO, csns[2]); assertModDNEntry(searchEntries.get(2), test + 4, test + "new4", test + "uuid4", CHANGENUMBER_ZERO, csns[3]); // check querying with cookie of add entry : should return 2 entries nbEntries = 2; searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[1], SUCCESS, nbEntries, test); // check querying with cookie of mod entry : should return 1 entry nbEntries = 1; searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[2], SUCCESS, nbEntries, test); searchEntries = searchOp.getSearchEntries(); assertModDNEntry(searchEntries.get(0), test + 4, test + "new4", test + "uuid4", CHANGENUMBER_ZERO, csns[3]); // check querying with cookie of mod dn entry : should return 0 entry nbEntries = 0; searchOp = searchChangelogUsingCookie("(targetdn=*" + test + "*,o=test)", cookies[3], SUCCESS, nbEntries, 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, server1); publishUpdateMessagesInOTest(test, true, generateDeleteMsg(server1, csns[0], test, 1), generateDeleteMsg(server1, csns[1], test, 2), generateDeleteMsg(server1, csns[2], test, 3)); InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*)", "", SUCCESS, 3, test); String firstCookie = readCookieFromNthEntry(searchOp.getSearchEntries(), 0); assertThat(firstCookie).isEqualTo(buildCookie(csns[0])); // remove the domain by sending a reset message publishUpdateMessages(test, server1, false, new ResetGenerationIdMsg(23657)); // replication changelog must have been cleared String cookie= ""; searchChangelogUsingCookie("(targetDN=*)", cookie, SUCCESS, 0, test); cookie = readLastCookieFromRootDSE(); searchChangelogUsingCookie("(targetDN=*)", cookie, SUCCESS, 0, test); // search with an old cookie searchOp = searchChangelogUsingCookie("(targetDN=*)", firstCookie, UNWILLING_TO_PERFORM, 0, test); assertThat(searchOp.getErrorMessage().toString()). contains("unknown replicated domain", DN_OTEST.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", "searchInChangeNumberModeOnOneSuffixMultipleTimes", "searchInChangeNumberModeOnOneSuffix", "searchInChangeNumberModeWithInvalidChangeNumber" }) 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++, server1.getServerId()); CSN csn2 = new CSN(time, seqNum++, server2.getServerId()); CSN csn3 = new CSN(time, seqNum++, server2.getServerId()); CSN csn4 = new CSN(time, seqNum++, server1.getServerId()); publishUpdateMessagesInOTest(test, false, generateDeleteMsg(server1, csn1, test, 1)); publishUpdateMessagesInOTest2(test, generateDeleteMsg(server2, csn2, test, 2), generateDeleteMsg(server2, csn3, test, 3)); publishUpdateMessagesInOTest(test, false, generateDeleteMsg(server1, csn4, test, 4)); // search on all suffixes using empty cookie String startCookie = ""; String cookie = startCookie; InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*)", cookie, SUCCESS, 4, test); cookie = readCookieFromNthEntry(searchOp.getSearchEntries(), 2); if (!new MultiDomainServerState(cookie).equals("o=test:" + csn1 + " " + csn3 + ";")) { // the changes were inserted in the DB while we were reading the results. // so they are not in the order expected by this test. // now that all the changes are in, retry, because they will now be returned in the expected order cookie = startCookie; searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*)", cookie, SUCCESS, 4, 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, SUCCESS, 1, test); cookie = assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn4); // publish a new change on first suffix CSN csn5 = new CSN(time, seqNum++, server1.getServerId()); publishUpdateMessagesInOTest(test, false, generateDeleteMsg(server1, csn5, test, 5)); // search using last cookie and expect to get the last change searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*)", cookie, SUCCESS, 1, test); assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn5); // search on first suffix only, with empty cookie cookie = ""; searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie, SUCCESS, 3, test); cookie = assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn1, csn4, csn5); // publish 4 more changes (2 on each suffix, on different server ids) time = TimeThread.getTime(); seqNum = 6; final ReplicaId server3 = ReplicaId.of(DN_OTEST2, 1203); final ReplicaId server4 = ReplicaId.of(DN_OTEST, 1204); CSN csn6 = new CSN(time, seqNum++, server3.getServerId()); CSN csn7 = new CSN(time, seqNum++, server4.getServerId()); CSN csn8 = new CSN(time, seqNum++, server3.getServerId()); CSN csn9 = new CSN(time, seqNum++, server4.getServerId()); publishUpdateMessages(test, server3, false, generateDeleteMsg(server3, csn6, test, 6)); publishUpdateMessages(test, server4, false, generateDeleteMsg(server4, csn7, test, 7)); publishUpdateMessages(test, server3, false, generateDeleteMsg(server3, csn8, test, 8)); publishUpdateMessages(test, server4, false, generateDeleteMsg(server4, csn9, test, 9)); // ensure oldest state is correct for each suffix and for each server id isOldestCSNForReplica(server1, csn1); isOldestCSNForReplica(server4, csn7); isOldestCSNForReplica(server2, csn2); isOldestCSNForReplica(server3, csn6); // test last cookie on root DSE String expectedLastCookie = "o=test:" + csn5 + " " + csn9 + ";o=test2:" + csn3 + " " + csn8 + ";"; final String lastCookie = assertLastCookieIsEqualTo(expectedLastCookie); // 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, UNWILLING_TO_PERFORM, 0, 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, SUCCESS, 5, test); assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn1, csn4, csn5, csn7, csn9); } finally { removeBackend(backendForSecondSuffix); } } private void isOldestCSNForReplica(final ReplicaId replicaId, final CSN csn) throws Exception { assertSameServerId(replicaId, csn); TestTimer timer = new TestTimer.Builder() .maxSleep(3, SECONDS) .sleepTimes(100, MILLISECONDS) .toTimer(); timer.repeatUntilSuccess(new Callable<Void>() { @Override public Void call() throws Exception { final ReplicationDomainDB domainDB = replicationServer.getChangelogDB().getReplicationDomainDB(); CursorOptions options = new CursorOptions(GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY); try (DBCursor<UpdateMsg> cursor = domainDB.getCursorFrom(replicaId.getBaseDN(), csn.getServerId(), csn, options)) { assertTrue(cursor.next(), "Expected to find at least one change in replicaDB for " + replicaId); assertEquals(cursor.getRecord().getCSN(), csn); return END_RUN; } } }); } @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; LDAPReplicationDomain domain2 = null; try { ReplicationBroker broker = enableReplication(server1); // create and publish 1 change on each suffix long time = TimeThread.getTime(); CSN csn1 = new CSN(time, 1, server1.getServerId()); broker.publish(generateDeleteMsg(server1, 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, newTreeSet("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, SUCCESS, 2, test); ExternalChangelogDomainCfg eclCfg = new ExternalChangelogDomainFakeCfg(false, null, null); domainConf2.setExternalChangelogDomain(eclCfg); domain2.applyConfigurationChange(domainConf2); // expect only entry from o=test returned searchChangelogUsingCookie("(targetDN=*)", cookie, SUCCESS, 1, test); // test the lastExternalChangelogCookie attribute of the ECL // (does only refer to non private backend) assertLastCookieIsEqualTo(buildCookie(csn1)); } finally { removeReplicationDomains(domain2); removeBackend(backend3); } debugInfo(test, "Ending test successfully"); } @Test public void searchInChangeNumberModeWithInvalidChangeNumber() 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 searchInChangeNumberModeOnOneSuffix() throws Exception { long firstChangeNumber = 1; String testName = "FourChanges/" + firstChangeNumber; debugInfo(testName, "Starting test\n\n"); CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(testName, false); searchChangesForEachOperationTypeUsingChangeNumberMode(firstChangeNumber, csns, testName); assertChangelogAttributesInRootDSE(1, 4); debugInfo(testName, "Ending search with success"); } @Test public void searchInChangeNumberModeOnOneSuffixMultipleTimes() 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); searchChangesForEachOperationTypeUsingChangeNumberMode(1, csns, testName); // write 4 more changes starting from changenumber 5, and search them testName = "Multiple/5"; csns = generateAndPublishUpdateMsgForEachOperationType(testName, false); searchChangesForEachOperationTypeUsingChangeNumberMode(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); assertChangelogAttributesInRootDSE(1, 8); // add a new change, then check again first and last change number without previous search testName = "Multiple/9"; CSN lastCsn = csns[csns.length - 1]; CSN csn = new CSN(lastCsn.getTime() + 1, 9, server1.getServerId()); publishUpdateMessagesInOTest(testName, false, generateDeleteMsg(server1, csn, testName, 1)); assertChangelogAttributesInRootDSE(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); SearchRequest request = Requests.newSearchRequest(DN.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE); InternalSearchOperation op = conn.processSearch(request); 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 = newTreeSet("localhost:" + replicationServerPort); // backend4 and domain4 backend4 = initializeMemoryBackend(false, backendId4); DomainFakeCfg domainConf = new DomainFakeCfg(baseDN4, 1702, replServers); SortedSet<String> eclInclude = newTreeSet("sn", "roomnumber"); domain4 = startNewReplicationDomain(domainConf, eclInclude, eclInclude); // backend5 and domain5 backend5 = initializeMemoryBackend(false, backendId5); domainConf = new DomainFakeCfg(baseDN5, 1703, replServers); eclInclude = newTreeSet("objectclass"); SortedSet<String> eclIncludeForDeletes = newTreeSet("*"); domain5 = startNewReplicationDomain(domainConf, eclInclude, eclIncludeForDeletes); // domain41 domainConf = new DomainFakeCfg(baseDN4, 1704, replServers); eclInclude = newTreeSet("cn"); domain41 = startNewReplicationDomain(domainConf, eclInclude, eclInclude); Thread.sleep(1000); addEntry(createEntry(baseDN4)); addEntry(createEntry(baseDN5)); Entry uentry1 = addEntry( "dn: cn=Fiona Jensen,o=" + backendId4, "objectclass: top", "objectclass: person", "objectclass: organizationalPerson", "objectclass: inetOrgPerson", "cn: Fiona Jensen", "sn: Jensen", "uid: fiona", "telephonenumber: 12121212"); Entry uentry2 = addEntry( "dn: cn=Robert Hue,o=" + backendId5, "objectclass: top", "objectclass: person", "objectclass: organizationalPerson", "objectclass: inetOrgPerson", "cn: Robert Hue", "sn: Robby", "uid: robert", "telephonenumber: 131313"); // 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, SUCCESS, 8, 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 = newHashSet("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 = newHashSet("firstchangenumber", "lastchangenumber", "changelog", "lastExternalChangelogCookie"); InternalSearchOperation searchOp = searchDNWithBaseScope(DN_OTEST, attributes); waitForSearchOpResult(searchOp, 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 filterString, long expectedFirstCN, long expectedLastCN) throws Exception { final ChangelogBackend backend = new ChangelogBackend(null, null); final DN baseDN = DN.valueOf(dn); final SearchFilter filter = SearchFilter.createFilterFromString(filterString); final ChangeNumberRange range = backend.optimizeSearch(baseDN, filter); assertChangeNumberRange(range, expectedFirstCN, expectedLastCN); } @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(); SearchFilter filter = SearchFilter.createFilterFromString("(replicationcsn=" + csn + ")"); final ChangeNumberRange range = backend.optimizeSearch(baseDN, filter); assertChangeNumberRange(range, -1, -1); } private List<SearchResultEntry> assertChangelogAttributesInRootDSE( final int expectedFirstChangeNumber, final int expectedLastChangeNumber) throws Exception { TestTimer timer = new TestTimer.Builder() .maxSleep(3, SECONDS) .sleepTimes(100, MILLISECONDS) .toTimer(); return timer.repeatUntilSuccess(new Callable<List<SearchResultEntry>>() { @Override public List<SearchResultEntry> call() throws Exception { final Set<String> attributes = new LinkedHashSet<>(); if (expectedFirstChangeNumber > 0) { attributes.add("firstchangenumber"); } attributes.add("lastchangenumber"); attributes.add("changelog"); attributes.add("lastExternalChangelogCookie"); final InternalSearchOperation searchOp = searchDNWithBaseScope(DN.rootDN(), attributes); final List<SearchResultEntry> entries = searchOp.getSearchEntries(); assertThat(entries).hasSize(1); final SearchResultEntry entry = entries.get(0); if (expectedFirstChangeNumber > 0) { assertAttributeValue(entry, "firstchangenumber", expectedFirstChangeNumber); } assertAttributeValue(entry, "lastchangenumber", expectedLastChangeNumber); assertAttributeValue(entry, "changelog", "cn=changelog"); assertNotNull(getAttributeValue(entry, "lastExternalChangelogCookie")); return entries; } }); } private String readLastCookieFromRootDSE() throws Exception { String cookie = ""; LDIFWriter ldifWriter = getLDIFWriter(); InternalSearchOperation searchOp = searchDNWithBaseScope(DN.rootDN(), newHashSet("lastExternalChangelogCookie")); List<SearchResultEntry> entries = searchOp.getSearchEntries(); if (entries != null) { for (SearchResultEntry resultEntry : entries) { ldifWriter.writeEntry(resultEntry); cookie = getAttributeValue(resultEntry, "lastexternalchangelogcookie"); } } return cookie; } private String assertLastCookieIsEqualTo(final String expectedLastCookie) throws Exception { TestTimer timer = new TestTimer.Builder() .maxSleep(1, SECONDS) .sleepTimes(10, MILLISECONDS) .toTimer(); return timer.repeatUntilSuccess(new Callable<String>() { @Override public String call() throws Exception { final String lastCookie = readLastCookieFromRootDSE(); assertThat(lastCookie).isEqualTo(expectedLastCookie); return lastCookie; } }); } private String assertLastCookieDifferentThanLastValue(final String notExpectedLastCookie) throws Exception { TestTimer timer = new TestTimer.Builder() .maxSleep(1, SECONDS) .sleepTimes(10, MILLISECONDS) .toTimer(); return timer.repeatUntilSuccess(new Callable<String>() { @Override public String call() throws Exception { final String lastCookie = readLastCookieFromRootDSE(); assertThat(lastCookie) .as("Expected last cookie to be updated, but it always stayed at value '" + notExpectedLastCookie + "'") .isNotEqualTo(notExpectedLastCookie); return lastCookie; } }); } 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)).containsOnly(csns); debugAndWriteEntries(ldifWriter, entries, test); return readCookieFromNthEntry(entries, csns.length - 1); } private List<CSN> getCSNsFromEntries(List<SearchResultEntry> entries) { List<CSN> results = new ArrayList<>(entries.size()); for (SearchResultEntry entry : entries) { results.add(new CSN(getAttributeValue(entry, "replicationCSN"))); } return results; } private void assertChangeNumberRange(ChangeNumberRange range, long firstChangeNumber, long lastChangeNumber) throws Exception { assertEquals(range.getLowerBound(), firstChangeNumber); assertEquals(range.getUpperBound(), lastChangeNumber); } private CSN[] generateAndPublishUpdateMsgForEachOperationType(String testName, boolean checkLastCookie) throws Exception { CSN[] csns = generateCSNs(4, server1); publishUpdateMessagesInOTest(testName, checkLastCookie, generateDeleteMsg(server1, csns[0], testName, 1), generateAddMsg(server1, csns[1], USER1_ENTRY_UUID, testName), generateModMsg(server1, csns[2], testName), generateModDNMsg(server1, 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, server1, checkLastCookie, messages); } private void publishUpdateMessagesInOTest2(String testName, UpdateMsg...messages) throws Exception { publishUpdateMessages(testName, server2, false, 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, ReplicaId replicaId, boolean checkLastCookie, ReplicationMsg... messages) throws Exception { ReplicationBroker broker = enableReplication(replicaId); String cookie = ""; for (ReplicationMsg msg : messages) { if (msg instanceof UpdateMsg) { final UpdateMsg updateMsg = (UpdateMsg) msg; assertThat(updateMsg.getCSN().getServerId()).isEqualTo(replicaId.getServerId()); debugInfo(testName, " publishes " + updateMsg.getCSN()); } broker.publish(msg); if (checkLastCookie) { cookie = assertLastCookieDifferentThanLastValue(cookie); } } } private String[] buildCookiesFromCsns(CSN[] csns) { final String[] cookies = new String[csns.length]; for (int j = 0; j < cookies.length; j++) { cookies[j] = buildCookie(csns[j]); } return cookies; } private void searchChangesForEachOperationTypeUsingChangeNumberMode(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); 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, ResultCode expectedResultCode, int expectedNbEntries, String testName) throws Exception { debugInfo(testName, "Search with cookie=[" + cookie + "] filter=[" + filterString + "]"); SearchRequest request = newSearchRequest(filterString).addControl(createCookieControl(cookie)); return searchChangelog(request, expectedNbEntries, expectedResultCode, testName); } private InternalSearchOperation searchChangelog(String filterString, int expectedNbEntries, ResultCode expectedResultCode, String testName) throws Exception { SearchRequest request = newSearchRequest(filterString); return searchChangelog(request, expectedNbEntries, expectedResultCode, testName); } private SearchRequest newSearchRequest(String filterString) throws DirectoryException { return Requests.newSearchRequest("cn=changelog", SearchScope.WHOLE_SUBTREE, filterString) .addAttribute("*", "+"); // all user and operational attributes } private InternalSearchOperation searchChangelog(final SearchRequest request, final int expectedNbEntries, final ResultCode expectedResultCode, String testName) throws Exception { TestTimer timer = new TestTimer.Builder() .maxSleep(5, SECONDS) .sleepTimes(10, MILLISECONDS) .toTimer(); InternalSearchOperation searchOp = timer.repeatUntilSuccess(new Callable<InternalSearchOperation>() { @Override public InternalSearchOperation call() throws Exception { InternalSearchOperation searchOp = connection.processSearch(request); final SoftAssertions softly = new SoftAssertions(); softly.assertThat(searchOp.getResultCode()).as(searchOp.getErrorMessage().toString()) .isEqualTo(expectedResultCode); softly.assertThat(searchOp.getSearchEntries()).hasSize(expectedNbEntries); softly.assertAll(); return searchOp; } }); debugAndWriteEntries(getLDIFWriter(), searchOp.getSearchEntries(), testName); return searchOp; } private InternalSearchOperation searchDNWithBaseScope(DN dn, Set<String> attributes) throws Exception { SearchRequest request = Requests.newSearchRequest(dn, SearchScope.BASE_OBJECT) .addAttribute(attributes); final InternalSearchOperation searchOp = connection.processSearch(request); 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 newArrayList(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, ReplicaId replicaId) { 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, replicaId.getServerId()); } return csns; } private UpdateMsg generateDeleteMsg(ReplicaId replicaId, CSN csn, String testName, int testIndex) throws Exception { assertSameServerId(replicaId, csn); String dn = "uid=" + testName + testIndex + "," + replicaId.getBaseDN(); return new DeleteMsg(DN.valueOf(dn), csn, testName + "uuid" + testIndex); } private UpdateMsg generateAddMsg(ReplicaId replicaId, CSN csn, String user1entryUUID, String testName) throws Exception { assertSameServerId(replicaId, csn); String baseUUID = "22222222-2222-2222-2222-222222222222"; String dn = "uid=" + testName + "2," + replicaId.getBaseDN(); Entry entry = makeEntry( "dn: " + dn, "objectClass: top", "objectClass: domain", "entryUUID: "+ user1entryUUID); return new AddMsg( csn, DN.valueOf(dn), user1entryUUID, baseUUID, entry.getObjectClassAttribute(), entry.getAttributes(), Collections.<Attribute> emptyList()); } private UpdateMsg generateModMsg(ReplicaId replicaId, CSN csn, String testName) throws Exception { assertSameServerId(replicaId, csn); DN baseDN = DN.valueOf("uid=" + testName + "3," + replicaId.getBaseDN()); 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(ReplicaId replicaId, CSN csn, String testName) throws Exception { assertSameServerId(replicaId, csn); final DN newSuperior = DN_OTEST2; ModifyDNOperation op = new ModifyDNOperationBasis(connection, 1, 1, null, DN.valueOf("uid=" + testName + "4," + replicaId.getBaseDN()), // 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); } private void assertSameServerId(ReplicaId replicaId, CSN csn) { assertThat(replicaId.getServerId()).isEqualTo(csn.getServerId()); } /** TODO : share this code with other classes ? */ private void waitForSearchOpResult(final Operation operation, final ResultCode expectedResult) throws Exception { TestTimer timer = new TestTimer.Builder() .maxSleep(500, MILLISECONDS) .sleepTimes(50, MILLISECONDS) .toTimer(); timer.repeatUntilSuccess(new Callable<Void>() { @Override public Void call() throws Exception { assertEquals(operation.getResultCode(), expectedResult, operation.getErrorMessage().toString()); return END_RUN; } }); } /** Verify that no entry contains the ChangeLogCookie control. */ private void assertContainsNoControl(InternalSearchOperation searchOp) { for (SearchResultEntry entry : searchOp.getSearchEntries()) { assertThat(entry.getControls()) .as("result entry " + entry + " should contain no control(s)") .isEmpty(); } } /** Verify that all entries contains the ChangeLogCookie control with the correct cookie value. */ private void assertResultsContainCookieControl(InternalSearchOperation searchOp, List<String> cookies) throws Exception { for (SearchResultEntry entry : searchOp.getSearchEntries()) { EntryChangelogNotificationControl cookieControl = getCookieControl(entry); assertNotNull(cookieControl, "result entry " + entry + " should contain the cookie control"); String cookieStr = cookieControl.getCookie().toString(); assertThat(cookieStr).isIn(cookies); cookies.remove(cookieStr); } assertThat(cookies).as("All cookie values should have been returned").isEmpty(); } private EntryChangelogNotificationControl getCookieControl(SearchResultEntry entry) { for (Control control : entry.getControls()) { if (OID_ECL_COOKIE_EXCHANGE_CONTROL.equals(control.getOID())) { return (EntryChangelogNotificationControl) control; } } return null; } /** Check the DEL entry has the right content. */ private void assertDelEntry(SearchResultEntry entry, String uid, String entryUUID, long changeNumber, CSN csn) throws Exception { assertAttributeValue(entry, "changetype", "delete"); assertAttributeValue(entry, "targetuniqueid", entryUUID); assertAttributeValue(entry, "targetentryuuid", entryUUID); assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn); } /** Check the ADD entry has the right content. */ private void assertAddEntry(SearchResultEntry entry, String uid, String entryUUID, long changeNumber, CSN csn) throws Exception { assertAttributeValue(entry, "changetype", "add"); assertEntryMatchesLDIF(entry, "changes", "objectClass: domain", "objectClass: top", "entryUUID: " + entryUUID); assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn); } private void assertModEntry(SearchResultEntry entry, String uid, String entryUUID, long changeNumber, CSN csn) throws Exception { assertAttributeValue(entry, "changetype", "modify"); assertEntryMatchesLDIF(entry, "changes", "replace: description", "description: new value", "-"); assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn); } private void assertModDNEntry(SearchResultEntry entry, String uid, String newUid, String entryUUID, long changeNumber, CSN csn) throws Exception { assertAttributeValue(entry, "changetype", "modrdn"); assertAttributeValue(entry, "newrdn", "uid=" + newUid); assertAttributeValue(entry, "newsuperior", DN_OTEST2); assertAttributeValue(entry, "deleteoldrdn", "true"); assertEntryCommonAttributes(entry, uid, entryUUID, changeNumber, csn); } private void assertEntryCommonAttributes(SearchResultEntry resultEntry, String uid, String entryUUID, long changeNumber, CSN csn) throws Exception { if (changeNumber == 0) { assertDNWithCSN(resultEntry, csn); } else { assertDNWithChangeNumber(resultEntry, changeNumber); assertAttributeValue(resultEntry, "changenumber", changeNumber); } assertAttributeValue(resultEntry, "targetentryuuid", entryUUID); assertAttributeValue(resultEntry, "replicaidentifier", SERVER_ID_1); assertAttributeValue(resultEntry, "replicationcsn", csn); assertAttributeValue(resultEntry, "changelogcookie", buildCookie(csn)); // A null value can be provided for uid if it should not be checked if (uid != null) { final String targetDN = "uid=" + uid + "," + DN_OTEST; 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); int idx = 0; assertDelEntry(entries.get(idx), testName + (idx + 1), testName + "uuid1", firstChangeNumber + idx, csns[idx]); idx = 1; assertAddEntry(entries.get(idx), testName + (idx + 1), entryUUID, firstChangeNumber + idx, csns[idx]); idx = 2; assertModEntry(entries.get(idx), testName + (idx + 1), testName + "uuid3", firstChangeNumber + idx, csns[idx]); idx = 3; assertModDNEntry(entries.get(idx), testName + (idx + 1), testName + "new4", testName + "uuid4", firstChangeNumber + idx, csns[idx]); } private String buildCookie(CSN csn) { return "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<>(); 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, Object expectedValue) { String expectedValueString = String.valueOf(expectedValue); assertFalse(expectedValueString.contains("\n"), "You should use assertEntryMatchesLDIF() method for asserting on this value: \"" + expectedValueString + "\""); final String actualValue = getAttributeValue(entry, attrName); assertThat(actualValue) .as("In entry " + entry + " incorrect value for attr '" + attrName + "'") .isEqualToIgnoringCase(expectedValueString); } private void assertDNWithChangeNumber(SearchResultEntry resultEntry, long changeNumber) throws Exception { DN actualDN = resultEntry.getName(); DN expectedDN = DN.valueOf("changenumber=" + changeNumber + ",cn=changelog"); assertThat(actualDN).isEqualTo(expectedDN); } private void assertDNWithCSN(SearchResultEntry resultEntry, CSN csn) throws Exception { DN actualDN = resultEntry.getName(); DN expectedDN = DN.valueOf("replicationcsn=" + csn + "," + DN_OTEST + ",cn=changelog"); assertThat(actualDN).isEqualTo(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> ldifEntryLines = new HashSet<>(); 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<>(); } } 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 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.openBackend(); 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); } @Override protected long getGenerationId(DN baseDN) { // Force value to ensure ReplicationBroker can connect to LDAPReplicationDomain, // even with multiple instances of each return TEST_DN_WITH_ROOT_ENTRY_GENID; } }