ChangelogBackendTestCase.java revision ea1068c292e9b341af6d6b563cd8988a96be20a9
/*
* 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 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;
{
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. */
{
ReplicationBroker broker = openReplicationSession(domainDN, serverId, 100, replicationPort, timeout);
}
/** 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 newDomain;
}
{
{
{
}
}
}
@Test
public void searchInCookieModeOnOneSuffixUsingEmptyCookie() throws Exception
{
int nbEntries = 4;
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]);
}
@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], nbEntries, SUCCESS, test);
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]);
// 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);
// 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);
}
@Test
public void searchInCookieModeAfterDomainIsRemoved() throws Exception
{
publishUpdateMessagesInOTest(test, true,
InternalSearchOperation searchOp = searchChangelogUsingCookie("(targetDN=*)", "", 3, SUCCESS, 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;
publishUpdateMessagesInOTest2(test, false,
// 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, 3, SUCCESS, test);
// publish 4 more changes (2 on each suffix, on differents server id)
seqNum = 6;
int serverId11 = 1203;
int serverId22 = 1204;
// 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, 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
// test missing domain in provided cookie
searchOp = searchChangelogUsingCookie("(targetDN=*" + test + "*,o=test)", cookie3, 0, UNWILLING_TO_PERFORM, test);
}
finally
{
}
}
{
domainDB.getCursorFrom(baseDN, csn.getServerId(), csn, GREATER_THAN_OR_EQUAL_TO_KEY, ON_MATCHING_KEY);
try {
"Expected to be to find at least one change in replicaDB(" + baseDN + " " + csn.getServerId() + ")");
}
finally
{
}
}
public void searchInCookieModeOnTwoSuffixesWithPrivateBackend() throws Exception
{
// Use o=test3 to avoid collision with o=test2 already used by a previous test
try {
replication1 = enableReplication(DN_OTEST, SERVER_ID_1, replicationServerPort, brokerSessionTimeout);
// 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
// check first and last change number
// add a new change, then check again first and last change number without previous search
publishUpdateMessagesInOTest(testName, false, generateDeleteMsg(TEST_ROOT_DN_STRING, csn, testName, 1));
}
/**
* 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, 8, SUCCESS, 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
{
}
{
{
try
{
if (expectedFirstChangeNumber > 0)
{
}
if (isECLEnabled)
{
if (expectedFirstChangeNumber > 0)
{
}
}
else
{
if (expectedFirstChangeNumber > 0) {
}
}
return entries;
}
catch (AssertionError ae)
{
// try again to see if changes have been persisted
}
}
throw error;
}
{
InternalSearchOperation searchOp = searchDNWithBaseScope("", newSet("lastExternalChangelogCookie"));
{
{
}
}
return cookie;
}
{
int count = 0;
while (count < 100)
{
{
return newCookie;
}
count++;
}
Assertions.fail("Expected last cookie should have been updated, but it always stayed at value '" + lastCookie + "'");
return null;// dead code
}
{
}
private String assertEntriesContainsCSNsAndReadLastCookie(String test, List<SearchResultEntry> entries,
{
}
{
{
}
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
{
}
private void publishUpdateMessagesInOTest2(String testName, boolean checkLastCookie, UpdateMsg...messages)
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
*/
private void publishUpdateMessages(String testName, DN baseDN, int serverId, boolean checkLastCookie,
{
try
{
replicationObjects = enableReplication(baseDN, serverId, replicationServerPort, brokerSessionTimeout);
{
{
}
if (checkLastCookie)
{
}
}
}
finally
{
if (replicationObjects != null)
{
}
}
}
{
{
}
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
{
}
{
}
{
}
{
int count = 0;
do
{
count++;
}
return searchOp;
}
private InternalSearchOperation searchDNWithBaseScope(String dn, Set<String> attributes) throws Exception
{
return searchOp;
}
/** Build a list of controls including the cookie provided. */
{
return newList(cookieControl);
}
{
return new LDIFWriter(exportConfig);
}
{
for (int i = 0; i < numberOfCsns; i++)
{
// seqNum must be greater than 0, so start at 1
}
return csns;
}
throws Exception
{
}
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 ?
{
int i = 0;
while (operation.getResultCode() == ResultCode.UNDEFINED || operation.getResultCode() != expectedResult)
{
i++;
if (i > 10)
{
}
}
}
/** 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,
{
assertDelEntry(entries.get(0), testName + "1", testName + "uuid1", firstChangeNumber, csn, "o=test:" + csn + ";");
assertAddEntry(entries.get(1), testName + "2", entryUUID, firstChangeNumber+1, csn, "o=test:" + csn + ";");
assertModDNEntry(entries.get(3), testName + "4", testName + "new4", testName + "uuid4", firstChangeNumber+3, csn,
}
/**
* 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: \"" + expectedValue + "\"");
}
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
}
}
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.
*/
{
}
}