ChangelogBackendTestCase.java revision 9a31b22460437b88cd6f4a751fe04c959239261a
/*
* 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
* 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.
*/
@SuppressWarnings("javadoc")
public class ChangelogBackendTestCase extends ReplicationTestCase
{
private static final long CHANGENUMBER_ZERO = 0L;
private static final int SERVER_ID_1 = 1201;
private static final int SERVER_ID_2 = 1202;
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;
{
super.setUp();
// This test suite depends on having the schema available.
}
public void classCleanUp() throws Exception
{
callParanoiaCheck = false;
super.classCleanUp();
}
public void clearReplicationDb() throws Exception
{
}
/** Configure a replicationServer for test. */
private void configureReplicationServer() throws Exception
{
"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()
{
{
}
});
}
/** Enable replication on provided domain DN and serverId, using provided port. */
{
{
broker = openReplicationSession(replicaId.getBaseDN(), replicaId.getServerId(), 100, replicationServerPort, 5000);
DomainFakeCfg domainConf = newFakeCfg(replicaId.getBaseDN(), replicaId.getServerId(), replicationServerPort);
}
return broker;
}
/** Start a new replication domain on the directory server side. */
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.
}
return domain;
}
{
{
{
}
}
}
@Test
public void searchInCookieModeOnOneSuffixUsingEmptyCookie() throws Exception
{
int nbEntries = 4;
assertModDNEntry(searchEntries.get(3), test + 4, test + "new4", test + "uuid4", CHANGENUMBER_ZERO, csns[3]);
}
@Test
public void searchInCookieModeOnOneSuffix() throws Exception
{
// 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);
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);
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);
}
@Test
public void searchInCookieModeAfterDomainIsRemoved() throws Exception
{
publishUpdateMessagesInOTest(test, true,
InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*)", "", SUCCESS, 3, test);
// remove the domain by sending a reset message
// replication changelog must have been cleared
// search with an old cookie
}
/**
* 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.
*/
"searchInCookieModeOnOneSuffixUsingEmptyCookie",
"searchInCookieModeOnOneSuffix",
"searchInCookieModeAfterDomainIsRemoved",
"searchInChangeNumberModeOnOneSuffixMultipleTimes",
"searchInChangeNumberModeOnOneSuffix",
"searchInChangeNumberModeWithInvalidChangeNumber" })
public void searchInCookieModeOnTwoSuffixes() throws Exception
{
try
{
// publish 4 changes (2 on each suffix)
int seqNum = 1;
// search on all suffixes using empty cookie
// search using previous cookie and expect to get ONLY the 4th change
cookie = assertEntriesContainsCSNsAndReadLastCookie(test, searchOp.getSearchEntries(), ldifWriter, csn4);
// publish a new change on first suffix
// search using last cookie and expect to get the last change
// search on first suffix only, with empty cookie
cookie = "";
searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie, SUCCESS, 3, test);
// publish 4 more changes (2 on each suffix, on different server ids)
seqNum = 6;
// ensure oldest state is correct for each suffix and for each server id
// test last cookie on root DSE
// 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 ?)
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
// test missing domain in provided cookie
searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie3, SUCCESS, 5, test);
}
finally
{
}
}
{
.toTimer();
{
{
try (DBCursor<UpdateMsg> cursor = domainDB.getCursorFrom(replicaId.getBaseDN(), csn.getServerId(), csn, options))
{
return END_RUN;
}
}
});
}
public void searchInCookieModeOnTwoSuffixesWithPrivateBackend() throws Exception
{
// Use o=test3 to avoid collision with o=test2 already used by a previous test
try {
// create and publish 1 change on each suffix
// create backend and configure replication for it
backend3.setPrivateBackend(true);
// add a root entry to the backend
// expect entry from o=test2 to be returned
// expect only entry from o=test returned
// test the lastExternalChangelogCookie attribute of the ECL
// (does only refer to non private backend)
}
finally
{
}
}
@Test
public void searchInChangeNumberModeWithInvalidChangeNumber() throws Exception
{
}
@Test
public void searchInChangeNumberModeOnOneSuffix() throws Exception
{
long firstChangeNumber = 1;
}
@Test
public void searchInChangeNumberModeOnOneSuffixMultipleTimes() throws Exception
{
// write 4 changes starting from changenumber 1, and search them
// write 4 more changes starting from changenumber 5, and search them
testName = "Multiple/5";
// search from the provided change number: 6 (should be the add msg)
// search from a provided change number interval: 5-7
// add a new change, then check again first and last change number without previous search
testName = "Multiple/9";
}
/** Verifies that is not possible to read the changelog without the changelog-read privilege. */
@Test
public void searchingWithoutPrivilegeShouldFail() throws Exception
{
SearchRequest request = Requests.newSearchRequest(DN.valueOf("cn=changelog"), SearchScope.WHOLE_SUBTREE);
assertEquals(op.getErrorMessage().toMessage(), NOTE_SEARCH_CHANGELOG_INSUFFICIENT_PRIVILEGES.get());
}
public void searchInCookieModeUseOfIncludeAttributes() throws Exception
{
// Use o=test4 and o=test5 to avoid collision with existing suffixes already used by previous test
try
{
// backend4 and domain4
// backend5 and domain5
// domain41
"dn: cn=Fiona Jensen,o=" + backendId4,
"objectclass: top",
"objectclass: person",
"objectclass: organizationalPerson",
"objectclass: inetOrgPerson",
"cn: Fiona Jensen",
"sn: Jensen",
"uid: fiona",
"telephonenumber: 12121212");
"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"));
// mod 'telephonenumber' of robert
// moddn robert to robert2
baseDN5);
// del robert
// Search on all suffixes
InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*)", cookie, SUCCESS, 8, test);
{
{
{
// We are using "*" for deletes so should get back 4 attributes.
}
else
{
}
}
{
}
assertAttributeValue(resultEntry,"changeinitiatorsname", "cn=Internal Client,cn=Root DNs,cn=config");
}
}
finally
{
}
}
/** With an empty RS, a search should return only root entry. */
@Test
public void searchWhenNoChangesShouldReturnRootEntryOnly() throws Exception
{
}
@Test
public void operationalAndVirtualAttributesShouldNotBeVisibleOutsideRootDSE() throws Exception
{
}
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 },
};
}
public void optimizeFiltersWithChangeNumber(String dn, String filterString, long expectedFirstCN, long expectedLastCN)
throws Exception
{
}
@Test
public void optimizeFiltersWithReplicationCsn() throws Exception
{
}
{
.toTimer();
{
{
if (expectedFirstChangeNumber > 0)
{
}
if (expectedFirstChangeNumber > 0)
{
}
return entries;
}
});
}
{
InternalSearchOperation searchOp = searchDNWithBaseScope(DN.rootDN(), newHashSet("lastExternalChangelogCookie"));
{
{
}
}
return cookie;
}
{
.toTimer();
{
{
return lastCookie;
}
});
}
private String assertLastCookieDifferentThanLastValue(final String notExpectedLastCookie) throws Exception
{
.toTimer();
{
{
.as("Expected last cookie to be updated, but it always stayed at value '" + notExpectedLastCookie + "'")
return lastCookie;
}
});
}
{
}
private String assertEntriesContainsCSNsAndReadLastCookie(String test, List<SearchResultEntry> entries,
{
// TODO JNR Should the order be guaranteed?
}
{
{
}
return results;
}
private void assertChangeNumberRange(ChangeNumberRange range, long firstChangeNumber, long lastChangeNumber)
throws Exception
{
}
private CSN[] generateAndPublishUpdateMsgForEachOperationType(String testName, boolean checkLastCookie)
throws Exception
{
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
{
}
throws Exception
{
}
/**
* 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
*/
{
{
{
}
if (checkLastCookie)
{
}
}
}
{
{
}
return cookies;
}
private void searchChangesForEachOperationTypeUsingChangeNumberMode(long firstChangeNumber, CSN[] csns,
{
// Search the changelog and check 4 entries are returned
assertEntriesForEachOperationType(searchOp.getSearchEntries(), firstChangeNumber, testName, USER1_ENTRY_UUID, csns);
// Search the changelog with filter on change number and check 4 entries are returned
filter =
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 searchChangelogFromToChangeNumber(int firstChangeNumber, int lastChangeNumber) throws Exception
{
String filter = "(&(changenumber>=" + firstChangeNumber + ")" + "(changenumber<=" + lastChangeNumber + "))";
}
throws Exception
{
}
{
}
{
}
private InternalSearchOperation searchChangelog(final SearchRequest request, final int expectedNbEntries,
{
.toTimer();
{
{
return searchOp;
}
});
return searchOp;
}
private InternalSearchOperation searchDNWithBaseScope(DN dn, Set<String> attributes) throws Exception
{
return searchOp;
}
/** Build a list of controls including the cookie provided. */
{
return newArrayList(cookieControl);
}
{
return new LDIFWriter(exportConfig);
}
{
for (int i = 0; i < numberOfCsns; i++)
{
// seqNum must be greater than 0, so start at 1
}
return csns;
}
private UpdateMsg generateDeleteMsg(ReplicaId replicaId, CSN csn, String testName, int testIndex) throws Exception
{
}
private UpdateMsg generateAddMsg(ReplicaId replicaId, CSN csn, String user1entryUUID, String testName) throws Exception
{
"dn: " + dn,
"objectClass: top",
"objectClass: domain",
"entryUUID: "+ user1entryUUID);
return new AddMsg(
csn,
}
{
}
{
}
{
true, // deleteoldrdn
return new ModifyDNMsg(localOp);
}
{
}
/** TODO : share this code with other classes ? */
private void waitForSearchOpResult(final Operation operation, final ResultCode expectedResult) throws Exception
{
.toTimer();
{
{
return END_RUN;
}
});
}
/** Verify that no entry contains the ChangeLogCookie control. */
{
{
.isEmpty();
}
}
/** Verify that all entries contains the ChangeLogCookie control with the correct cookie value. */
private void assertResultsContainCookieControl(InternalSearchOperation searchOp, String[] cookies) throws Exception
{
{
boolean cookieControlFound = false;
{
{
cookieControlFound = true;
}
}
}
}
/** Check the DEL entry has the right content. */
{
}
/** Check the ADD entry has the right content. */
{
"objectClass: domain",
"objectClass: top",
"entryUUID: " + entryUUID);
}
{
"replace: description",
"description: new value",
"-");
}
{
}
{
if (changeNumber == 0)
{
}
else
{
}
// A null value can be provided for uid if it should not be checked
{
}
}
private void assertEntriesForEachOperationType(List<SearchResultEntry> entries, long firstChangeNumber,
{
int idx = 0;
testName + "uuid1",
idx = 1;
idx = 2;
testName + "uuid3",
idx = 3;
}
{
}
/** Asserts the attribute value as LDIF to ignore lines ordering. */
private static void assertEntryMatchesLDIF(Entry entry, String attrName, String... expectedLDIFLines)
{
}
{
{
{
}
}
}
{
"You should use assertEntryMatchesLDIF() method for asserting on this value: \"" + expectedValueString + "\"");
}
private void assertDNWithChangeNumber(SearchResultEntry resultEntry, long changeNumber) throws Exception
{
}
{
}
/**
* 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>
*/
{
{
{
// same entry keep adding
}
else
{
// this is a new entry
ldifEntryLines = new HashSet<>();
}
}
return results;
}
{
{
return null;
}
}
private Entry parseIncludedAttributes(SearchResultEntry resultEntry, String targetdn) throws Exception
{
// Parse includedAttributes as an entry.
}
private void debugAndWriteEntries(LDIFWriter ldifWriter,List<SearchResultEntry> entries, String tn) throws Exception
{
{
{
// Can use entry.toSingleLineString()
if (ldifWriter != null)
{
}
}
}
}
/** Creates a memory backend, to be used as additional backend in tests. */
private static Backend<?> initializeMemoryBackend(boolean createBaseEntry, String backendId) throws Exception
{
// 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.
if (memoryBackend == null)
{
memoryBackend = new MemoryBackend();
}
if (createBaseEntry)
{
}
return memoryBackend;
}
{
{
{
}
}
}
/**
* Utility - log debug message - highlight it is from the test and not
* from the server code. Makes easier to observe the test steps.
*/
{
}
{
// Force value to ensure ReplicationBroker can connect to LDAPReplicationDomain,
// even with multiple instances of each
return TEST_DN_WITH_ROOT_ENTRY_GENID;
}
}