/*
* 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 2015 ForgeRock AS
*/
package org.opends.server.backends.pluggable;
import static org.assertj.core.api.Assertions.assertThat;
import static org.forgerock.opendj.ldap.ModificationType.ADD;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.opends.server.protocols.internal.InternalClientConnection.getRootConnection;
import static org.opends.server.protocols.internal.Requests.newSearchRequest;
import static org.opends.server.types.Attributes.create;
import static org.opends.server.types.IndexType.*;
import static org.opends.server.util.CollectionUtils.*;
import static org.testng.Assert.*;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ConditionResult;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.SearchScope;
import org.forgerock.util.Reject;
import org.opends.server.DirectoryServerTestCase;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.std.meta.BackendIndexCfgDefn.IndexType;
import org.opends.server.admin.std.meta.BackendVLVIndexCfgDefn.Scope;
import org.opends.server.admin.std.server.BackendIndexCfg;
import org.opends.server.admin.std.server.BackendVLVIndexCfg;
import org.opends.server.admin.std.server.PluggableBackendCfg;
import org.opends.server.api.Backend.BackendOperation;
import org.opends.server.api.ClientConnection;
import org.opends.server.backends.RebuildConfig;
import org.opends.server.backends.VerifyConfig;
import org.opends.server.backends.RebuildConfig.RebuildMode;
import org.opends.server.backends.pluggable.spi.AccessMode;
import org.opends.server.backends.pluggable.spi.ReadOnlyStorageException;
import org.opends.server.backends.pluggable.spi.ReadOperation;
import org.opends.server.backends.pluggable.spi.ReadableTransaction;
import org.opends.server.backends.pluggable.spi.Storage;
import org.opends.server.backends.pluggable.spi.TreeName;
import org.opends.server.backends.pluggable.spi.WriteOperation;
import org.opends.server.backends.pluggable.spi.WriteableTransaction;
import org.opends.server.core.DirectoryServer;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.internal.SearchRequest;
import org.opends.server.types.AttributeType;
import org.opends.server.types.BackupConfig;
import org.opends.server.types.BackupDirectory;
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.LDIFImportConfig;
import org.opends.server.types.Modification;
import org.opends.server.types.RestoreConfig;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation;
import org.testng.Reporter;
import org.testng.annotations.AfterClass;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
/**
* Unit tests for pluggable backend implementations. The test methods have side-effects and must be run in-order.
*/
@SuppressWarnings("javadoc")
@Test(groups = { "precommit", "pluggablebackend" }, sequential = true)
public abstract class PluggableBackendImplTestCase<C extends PluggableBackendCfg> extends DirectoryServerTestCase
{
private BackendImpl<C> backend;
private List<Entry> topEntries;
private List<Entry> entries;
private List<Entry> workEntries;
private DN testBaseDN;
private DN dnToDel;
private DN searchDN;
private DN badEntryDN;
private String backupID;
private Map<String, IndexType[]> backendIndexes = new HashMap<>();
{
backendIndexes.put("entryUUID", new IndexType[] { IndexType.EQUALITY });
backendIndexes.put("cn", new IndexType[] { IndexType.SUBSTRING });
backendIndexes.put("sn", new IndexType[] { IndexType.PRESENCE, IndexType.EQUALITY, IndexType.SUBSTRING });
backendIndexes.put("uid", new IndexType[] { IndexType.EQUALITY });
backendIndexes.put("telephoneNumber", new IndexType[] { IndexType.EQUALITY, IndexType.SUBSTRING });
backendIndexes.put("mail", new IndexType[] { IndexType.SUBSTRING });
}
private String[] backendVlvIndexes = { "people" };
private AttributeType modifyAttribute;
private final ByteString modifyValue = ByteString.valueOfUtf8("foo");
private BackupDirectory backupDirectory;
/**
* Factory method for creating a new unconfigured backend instance.
*
* @return the unconfigured backend instance.
* @see #setUp()
*/
protected abstract BackendImpl<C> createBackend();
/**
* Factory method for creating a new backend configuration. All methods specific to the sub-class
* should be stubbed out.
*
* @return the new backend configuration.
* @see #setUp()
*/
protected abstract C createBackendCfg();
@BeforeClass
public void setUp() throws Exception
{
// Need the schema to be available, so make sure the server is started.
TestCaseUtils.startServer();
testBaseDN = DN.valueOf("dc=test,dc=com");
C backendCfg = createBackendCfg();
when(backendCfg.dn()).thenReturn(testBaseDN);
when(backendCfg.getBaseDN()).thenReturn(newTreeSet(testBaseDN));
when(backendCfg.listBackendIndexes()).thenReturn(backendIndexes.keySet().toArray(new String[0]));
when(backendCfg.listBackendVLVIndexes()).thenReturn(backendVlvIndexes);
for (Map.Entry<String, IndexType[]> index : backendIndexes.entrySet())
{
final String attributeName = index.getKey().toLowerCase();
final AttributeType attribute = DirectoryServer.getAttributeTypeOrNull(attributeName);
Reject.ifNull(attribute, "Attribute type '" + attributeName + "' doesn't exists.");
BackendIndexCfg indexCfg = mock(BackendIndexCfg.class);
when(indexCfg.getIndexType()).thenReturn(newTreeSet(index.getValue()));
when(indexCfg.getAttribute()).thenReturn(attribute);
when(indexCfg.getIndexEntryLimit()).thenReturn(4000);
when(indexCfg.getSubstringLength()).thenReturn(6);
when(backendCfg.getBackendIndex(index.getKey())).thenReturn(indexCfg);
}
BackendVLVIndexCfg vlvIndexCfg = mock(BackendVLVIndexCfg.class);
when(vlvIndexCfg.getName()).thenReturn("people");
when(vlvIndexCfg.getBaseDN()).thenReturn(testBaseDN);
when(vlvIndexCfg.getFilter()).thenReturn("(objectClass=person)");
when(vlvIndexCfg.getScope()).thenReturn(Scope.WHOLE_SUBTREE);
when(vlvIndexCfg.getSortOrder()).thenReturn("sn -employeeNumber +uid");
when(backendCfg.getBackendVLVIndex(backendVlvIndexes[0])).thenReturn(vlvIndexCfg);
backend = createBackend();
backend.setBackendID(backendCfg.getBackendId());
backend.configureBackend(backendCfg, DirectoryServer.getInstance().getServerContext());
backend.openBackend();
topEntries = TestCaseUtils.makeEntries(
"dn: " + testBaseDN,
"objectclass: top",
"objectclass: domain",
"",
"dn: ou=People," + testBaseDN,
"objectclass: top",
"objectclass: organizationalUnit",
"ou: People");
entries = TestCaseUtils.makeEntries(
"dn: uid=user.0,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Aaccf",
"sn: Amar",
"cn: Aaccf Amar",
"initials: AQA",
"employeeNumber: 0",
"uid: user.0",
"mail: user.0@example.com",
"userPassword: password",
"telephoneNumber: 380-535-2354",
"homePhone: 707-626-3913",
"pager: 456-345-7750",
"mobile: 366-674-7274",
"street: 99262 Eleventh Street",
"l: Salem",
"st: NM",
"postalCode: 36530",
"postalAddress: Aaccf Amar$99262 Eleventh Street$Salem, NM 36530",
"description: This is the description for Aaccf Amar.",
"",
"dn: uid=user.1,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Aaren",
"givenName;lang-fr: test2",
"givenName;lang-cn: test2",
"givenName;lang-es: test3",
"sn: Atp",
"cn: Aaren Atp",
"initials: APA",
"employeeNumber: 1",
"uid: user.1",
"mail: user.1@example.com",
"userPassword: password",
"telephoneNumber: 643-278-6134",
"homePhone: 546-786-4099",
"pager: 508-261-3187",
"mobile: 377-267-7824",
"carLicense: 377-267-7824",
"street: 78113 Fifth Street",
"l: Chico",
"st: TN",
"postalCode: 72322",
"postalAddress: Aaren Atp$78113 Fifth Street$Chico, TN 72322",
"description: This is the description for Aaren Atp.",
"",
"dn: uid=user.2,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Aarika",
"sn: Atpco",
"cn: Aarika Atpco",
"initials: ARA",
"employeeNumber: 2",
"uid: user.2",
"mail: user.2@example.com",
"userPassword: password",
"telephoneNumber: 547-504-3498",
"homePhone: 955-899-7308",
"pager: 710-832-9316",
"mobile: 688-388-4525",
"carLicense: 688-388-4525",
"street: 59208 Elm Street",
"l: Youngstown",
"st: HI",
"postalCode: 57377",
"postalAddress: Aarika Atpco$59208 Elm Street$Youngstown, HI 57377",
"description: This is the description for Aarika Atpco.",
"",
"dn: uid=user.3,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Aaron",
"sn: Atrc",
"cn: Aaron Atrc",
"initials: AIA",
"employeeNumber: 3",
"uid: user.3",
"mail: user.3@example.com",
"userPassword: password",
"telephoneNumber: 128-108-4939",
"homePhone: 512-782-9966",
"pager: 322-646-5118",
"mobile: 360-957-9137",
"carLicense: 360-957-9137",
"street: 25074 Hill Street",
"l: Toledo",
"st: OR",
"postalCode: 55237",
"postalAddress: Aaron Atrc$25074 Hill Street$Toledo, OR 55237",
"description: This is the description for Aaron Atrc.",
"",
"dn: uid=user.4,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Aartjan",
"sn: Aalders",
"cn: Aartjan Aalders",
"initials: ALA",
"employeeNumber: 4",
"uid: user.4",
"mail: user.4@example.com",
"userPassword: password",
"telephoneNumber: 981-148-3303",
"homePhone: 196-877-2684",
"pager: 910-998-4607",
"mobile: 123-239-8262",
"carLicense: 123-239-8262",
"street: 81512 Sunset Street",
"l: Chattanooga",
"st: WV",
"postalCode: 29530",
"postalAddress: Aartjan Aalders$81512 Sunset Street$Chattanooga, WV 29530",
"description: This is the description for Aartjan Aalders.",
"",
"dn: uid=user.5,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Abagael",
"sn: Aasen",
"cn: Abagael Aasen",
"initials: AKA",
"employeeNumber: 5",
"uid: user.5",
"mail: user.5@example.com",
"userPassword: password",
"telephoneNumber: 930-493-2391",
"homePhone: 078-254-3960",
"pager: 281-936-8197",
"mobile: 559-822-7712",
"carLicense: 559-822-7712",
"street: 31988 Central Street",
"l: Chico",
"st: MS",
"postalCode: 20135",
"postalAddress: Abagael Aasen$31988 Central Street$Chico, MS 20135",
"description: This is the description for Abagael Aasen.",
"",
"dn: uid=user.6,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Abagail",
"sn: Abadines",
"cn: Abagail Abadines",
"initials: AQA",
"employeeNumber: 6",
"uid: user.6",
"mail: user.6@example.com",
"userPassword: password",
"telephoneNumber: 110-761-3861",
"homePhone: 459-123-0553",
"pager: 799-151-2688",
"mobile: 953-582-7252",
"carLicense: 953-582-7252",
"street: 60100 Dogwood Street",
"l: Hartford",
"st: NE",
"postalCode: 79353",
"postalAddress: Abagail Abadines$60100 Dogwood Street$Hartford, NE 79353",
"description: This is the description for Abagail Abadines.",
"",
"dn: uid=user.7,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Abahri",
"sn: Abazari",
"cn: Abahri Abazari",
"initials: AXA",
"employeeNumber: 7",
"uid: user.7",
"mail: user.7@example.com",
"userPassword: password",
"telephoneNumber: 594-537-4292",
"homePhone: 174-724-6390",
"pager: 733-217-8194",
"mobile: 879-706-0172",
"carLicense: 879-706-0172",
"street: 77693 Oak Street",
"l: Philadelphia",
"st: MN",
"postalCode: 78550",
"postalAddress: Abahri Abazari$77693 Oak Street$Philadelphia, MN 78550",
"description: This is the description for Abahri Abazari.",
"",
"dn: uid=user.8,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Abbas",
"sn: Abbatantuono",
"cn: Abbas Abbatantuono",
"initials: AVA",
"employeeNumber: 8",
"uid: user.8",
"mail: user.8@example.com",
"userPassword: password",
"telephoneNumber: 246-674-8407",
"homePhone: 039-769-3372",
"pager: 226-950-2371",
"mobile: 587-709-2996",
"carLicense: 587-709-2996",
"street: 23230 Hill Street",
"l: Little Rock",
"st: AR",
"",
"dn: uid=user.9,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Abbe",
"sn: Abbate",
"cn: Abbe Abbate",
"initials: AWA",
"employeeNumber: 9",
"uid: user.9",
"mail: user.9@example.com",
"userPassword: password",
"telephoneNumber: 205-805-3357",
"homePhone: 770-780-5917",
"pager: 537-074-8005",
"mobile: 120-204-7597",
"carLicense: 120-204-7597",
"street: 47952 Center Street",
"l: Butte",
"st: TN",
"postalCode: 69384",
"postalAddress: Abbe Abbate$47952 Center Street$Butte, TN 69384",
"description: This is the description for Abbe Abbate.",
"",
"dn: uid=user.10,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Abbey",
"sn: Abbie",
"cn: Abbey Abbie",
"initials: AZA",
"employeeNumber: 10",
"uid: user.10",
"mail: user.10@example.com",
"userPassword: password",
"telephoneNumber: 457-819-0832",
"homePhone: 931-305-5452",
"pager: 118-165-7194",
"mobile: 553-729-5572",
"carLicense: 553-729-5572",
"street: 54262 Highland Street",
"l: Spartanburg",
"st: PA",
"postalCode: 38151",
"postalAddress: Abbey Abbie$54262 Highland Street$Spartanburg, PA 38151",
"description: This is the description for Abbey Abbie.",
"",
"dn: uid=user.539,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Ardyth",
"sn: Bainton",
"cn: Ardyth Bainton",
"initials: AIB",
"employeeNumber: 539",
"uid: user.539",
"mail: user.539@example.com",
"userPassword: password",
"telephoneNumber: 641-433-7404",
"homePhone: 524-765-8780",
"pager: 985-331-1308",
"mobile: 279-423-0188",
"carLicense: 279-423-0188",
"street: 81170 Taylor Street",
"l: Syracuse",
"st: WV",
"postalCode: 93507",
"postalAddress: Ardyth Bainton$81170 Taylor Street$Syracuse, WV 93507",
"description: This is the description for Ardyth Bainton.");
workEntries = TestCaseUtils.makeEntries(
"dn: uid=user.11,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Annalee",
"sn: Avard",
"cn: Annalee Avard",
"initials: ANA",
"employeeNumber: 11",
"uid: user.11",
"mail: user.11@example.com",
"userPassword: password",
"telephoneNumber: 875-335-2712",
"homePhone: 181-995-6635",
"pager: 586-905-4185",
"mobile: 826-857-7592",
"carLicense: 826-857-7592",
"street: 46168 Mill Street",
"l: Charleston",
"st: CO",
"postalCode: 60948",
"postalAddress: Annalee Avard$46168 Mill Street$Charleston, CO 60948",
"description: This is the description for Annalee Avard.",
"",
"dn: uid=user.12,ou=People," + testBaseDN,
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"givenName: Andaree",
"sn: Asawa",
"cn: Andaree Asawa",
"initials: AEA",
"employeeNumber: 12",
"uid: user.12",
"mail: user.12@example.com",
"userPassword: password",
"telephoneNumber: 399-788-7334",
"homePhone: 798-076-5683",
"pager: 034-026-9411",
"mobile: 948-743-9197",
"carLicense: 948-743-9197",
"street: 81028 Forest Street",
"l: Wheeling",
"st: IA",
"postalCode: 60905",
"postalAddress: Andaree Asawa$81028 Forest Street$Wheeling, IA 60905",
"description: This is the description for Andaree Asawa.");
dnToDel = workEntries.get(1).getName();
searchDN = entries.get(1).getName();
badEntryDN = testBaseDN.child(DN.valueOf("ou=bogus")).child(DN.valueOf("ou=dummy"));
backupID = "backupID1";
addEntriesToBackend(topEntries);
addEntriesToBackend(entries);
addEntriesToBackend(workEntries);
}
private void addEntriesToBackend(List<Entry> entries) throws Exception
{
for (Entry newEntry : entries)
{
backend.addEntry(newEntry, null);
}
}
@AfterClass
public void cleanUp() throws Exception
{
try
{
if (backupDirectory != null)
{
backend.removeBackup(backupDirectory, backupID);
}
}
catch (DirectoryException ignore)
{
/*
* Due to test sequencing (Import, backup, restore, export) in case of intermediate failure
* Backup may or may not be present. -> ignore.
*/
Reporter.log(ignore.getMessage(), true);
}
finally
{
backend.finalizeBackend();
backend = null;
}
}
/**
* Tests the storage API for resource checking. The tested method has no return value, but an
* exception, while not systematic, may be thrown, in which case the test must fail.
*
* @throws Exception
* if resources are low.
*/
@Test
public void testCheckForEnoughResources() throws Exception
{
backend.getRootContainer().checkForEnoughResources(null);
}
@Test(expectedExceptions = DirectoryException.class)
public void testAddNoParent() throws Exception
{
Entry newEntry = TestCaseUtils.makeEntry("dn: " + badEntryDN, "objectclass: ou", "");
backend.addEntry(newEntry, null);
}
@Test
public void testUtilityAPIs()
{
assertEquals(backend.getEntryCount(), getTotalNumberOfLDIFEntries());
assertFalse(backend.isIndexed(modifyAttribute, EQUALITY));
for (Map.Entry<String, IndexType[]> index : backendIndexes.entrySet())
{
for (IndexType type : index.getValue())
{
final AttributeType attributeType = DirectoryServer.getAttributeTypeOrNull(index.getKey().toLowerCase());
assertTrue(backend.isIndexed(attributeType,
org.opends.server.types.IndexType.valueOf(type.toString().toUpperCase())));
}
}
}
private int getTotalNumberOfLDIFEntries()
{
return topEntries.size() + entries.size() + workEntries.size();
}
@Test
public void testHasSubordinates() throws Exception
{
assertEquals(backend.hasSubordinates(testBaseDN), ConditionResult.TRUE,
"Base DN should have subordinates.");
// Testing ConditionResult.UNDEFINED needs either no entry container or a big DIT...
assertEquals(backend.hasSubordinates(DN.valueOf("dc=a")), ConditionResult.UNDEFINED,
"Subordinates query on unknown baseDN should return UNDEFINED.");
assertEquals(backend.getNumberOfChildren(testBaseDN), 1);
assertEquals(backend.getNumberOfEntriesInBaseDN(testBaseDN), getTotalNumberOfLDIFEntries(), "Wrong DIT count.");
assertEquals(backend.hasSubordinates(searchDN), ConditionResult.FALSE,
"Leaf entry should not have any subordinates.");
}
private List<SearchResultEntry> runSearch(SearchRequest request, boolean useInternalConnection) throws Exception
{
InternalClientConnection conn = getRootConnection();
if (useInternalConnection)
{
InternalSearchOperation search = conn.processSearch(request);
return search.getSearchEntries();
}
else
{
InternalSearchOperation search = new InternalSearchOperation(conn, -1, -1, request);
backend.search(new LocalBackendSearchOperation(search));
return search.getSearchEntries();
}
}
@Test
public void testModifyEntry() throws Exception
{
Entry oldEntry = workEntries.get(0);
Entry newEntry = oldEntry.duplicate(false);
modifyAttribute = DirectoryServer.getAttributeTypeOrNull("jpegphoto");
newEntry.applyModifications(Arrays.asList(new Modification(ADD, create(modifyAttribute, modifyValue))));
backend.replaceEntry(oldEntry, newEntry, null);
assertTrue(backend.getEntry(oldEntry.getName()).hasValue(modifyAttribute, null, modifyValue));
}
@Test
public void testRenameEntry() throws Exception
{
// Move the entire subtree to another name and move it back.
DN prevDN = DN.valueOf("ou=People," + testBaseDN);
DN newDN = DN.valueOf("ou=users," + testBaseDN);
Entry renameEntry = backend.getEntry(prevDN).duplicate(false);
renameEntry.setDN(newDN);
backend.renameEntry(prevDN, renameEntry, null);
Entry dbEntry = backend.getEntry(newDN);
assertEquals(dbEntry.getName(), newDN, "Renamed entry is missing.");
renameEntry.setDN(prevDN);
backend.renameEntry(newDN, renameEntry, null);
dbEntry = backend.getEntry(prevDN);
assertEquals(dbEntry.getName(), prevDN, "Original entry has not been renamed");
}
@Test(description = "OPENDJ-2404")
public void testRenameEntrySameDNDifferentCase() throws Exception
{
DN prevDN = DN.valueOf("uid=user.0,ou=People," + testBaseDN);
DN newDN = DN.valueOf("uid=USER.0,ou=People," + testBaseDN);
Entry renameEntry = backend.getEntry(prevDN).duplicate(false);
renameEntry.setDN(newDN);
backend.renameEntry(prevDN, renameEntry, null);
Entry dbEntry = backend.getEntry(newDN);
assertEquals(dbEntry.getName().rdn().toString(), "uid=USER.0");
renameEntry.setDN(prevDN);
backend.renameEntry(newDN, renameEntry, null);
dbEntry = backend.getEntry(prevDN);
assertEquals(dbEntry.getName().rdn().toString(), "uid=user.0");
}
@Test
public void testDeleteEntry() throws Exception
{
Entry deletedEntry = backend.getEntry(dnToDel).duplicate(false);
deleteEntry(dnToDel);
try
{
deleteEntry(dnToDel);
fail("Should have generated a DirectoryException");
}
catch (DirectoryException de)
{
// Expected exception, do nothing, test succeeds.
}
finally
{
backend.addEntry(deletedEntry, null);
}
}
private void deleteEntry(DN dn) throws Exception
{
backend.deleteEntry(dn, null);
assertNull(backend.getEntry(workEntries.get(1).getName()));
}
@Test
public void testBaseSearch() throws Exception
{
baseSearch(false);
baseSearch(true);
}
private void baseSearch(boolean useInternalConnection) throws Exception
{
SearchRequest request = newSearchRequest(testBaseDN, SearchScope.BASE_OBJECT, "objectclass=*");
List<SearchResultEntry> result = runSearch(request, useInternalConnection);
assertEquals(result.size(), 1, "Base Search should return only one Entry");
assertEquals(result.get(0).getName(), testBaseDN, "Base Search on the suffix should return the suffix itself");
}
@Test
public void testOneLevelSearch() throws Exception
{
oneLevelSearch(false);
oneLevelSearch(true);
}
private void oneLevelSearch(boolean useInternalConnection) throws Exception
{
SearchRequest request = newSearchRequest(testBaseDN, SearchScope.SINGLE_LEVEL, "objectclass=*");
List<SearchResultEntry> result = runSearch(request, useInternalConnection);
assertEquals(result.size(), 1, "One Level search should return a single child entry");
SearchResultEntry resEntry = result.get(0);
assertEquals(topEntries.get(1).getName(), resEntry.getName(),
"One Level search should return the expected child");
}
@Test
public void testSubTreeSearch() throws Exception
{
subTreeSearch(false);
subTreeSearch(true);
}
@Test
public void testSubTreeSearchAgainstAnIndexWithUnrecognizedMatchingRule() throws Exception
{
SearchRequest request = newSearchRequest(testBaseDN, SearchScope.WHOLE_SUBTREE, "entryUUID=xxx*");
assertThat(runSearch(request, false)).isEmpty();
}
@Test
public void testSearchIsConsideredUnindexedBasedOnLookThroughLimit() throws DirectoryException {
final int nbEntries = topEntries.size() + entries.size() + workEntries.size();
final SearchRequest request = newSearchRequest(testBaseDN, SearchScope.WHOLE_SUBTREE, "objectclass=*");
final ClientConnection connection = new ClientConnectionStub();
connection.setLookthroughLimit(0);
InternalSearchOperation searchOperation = new InternalSearchOperation(connection, 1, 1, request, null);
searchOperation.run();
assertThat(searchOperation.getEntriesSent()).isEqualTo(nbEntries);
connection.setLookthroughLimit(nbEntries);
searchOperation = new InternalSearchOperation(connection, 1, 1, request, null);
searchOperation.run();
assertThat(searchOperation.getEntriesSent()).isEqualTo(nbEntries);
connection.setLookthroughLimit(nbEntries - 1);
searchOperation = new InternalSearchOperation(connection, 1, 1, request, null);
searchOperation.run();
assertThat(searchOperation.getResultCode()).isEqualTo(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
assertThat(searchOperation.getErrorMessage().toString()).contains("not have sufficient privileges", "unindexed search");
assertThat(searchOperation.getEntriesSent()).isEqualTo(0);
}
private void subTreeSearch(boolean useInternalConnection) throws Exception
{
SearchRequest request = newSearchRequest(testBaseDN, SearchScope.WHOLE_SUBTREE, "objectclass=*");
List<SearchResultEntry> result = runSearch(request, useInternalConnection);
// Sum of all entry sets minus a delete
assertEquals(result.size(), getTotalNumberOfLDIFEntries(),
"Subtree search should return a correct number of entries");
}
@DataProvider(name = "userEntrySearchData")
protected Object[][] userEntrySearchData()
{
return new Object[][] {
// @formatter:off
{ true, SearchScope.BASE_OBJECT, 1 },
{ false, SearchScope.BASE_OBJECT, 1 },
{ true, SearchScope.SINGLE_LEVEL, 0 },
{ false, SearchScope.SINGLE_LEVEL, 0 },
{ true, SearchScope.WHOLE_SUBTREE, 1 },
{ false, SearchScope.WHOLE_SUBTREE, 1 },
// @formatter:on
};
}
@Test(dataProvider = "userEntrySearchData")
public void testUserEntrySearch(boolean useInternalConnection, SearchScope scope, int expectedEntryCount)
throws Exception
{
SearchRequest request = newSearchRequest(searchDN, scope, "objectclass=*");
List<SearchResultEntry> result = runSearch(request, useInternalConnection);
assertEquals(result.size(), expectedEntryCount, "User entry search should return " + expectedEntryCount
+ " child entry");
if (expectedEntryCount > 0)
{
assertEquals(searchDN, result.get(0).getName(), "User entry search should return the expected entry");
}
}
@Test
public void testGetEntry() throws Exception
{
for (Entry expected : entries)
{
Entry dbEntry = backend.getEntry(expected.getName());
Entry actual = new Entry(dbEntry.getName(), dbEntry.getObjectClasses(), dbEntry.getUserAttributes(), null);
// Remove the userPassword because it will have been encoded.
expected.removeAttribute(DirectoryServer.getAttributeTypeOrDefault("userpassword"));
actual.removeAttribute(DirectoryServer.getAttributeTypeOrDefault("userpassword"));
assertThat(actual).isEqualTo(expected);
}
}
@Test
public void testExportLDIFAndImportLDIF() throws Exception
{
assertTrue(backend.supports(BackendOperation.LDIF_EXPORT), "Export not supported");
ByteArrayOutputStream ldifOutputContent = new ByteArrayOutputStream();
try (final LDIFExportConfig exportConfig = new LDIFExportConfig(ldifOutputContent))
{
exportConfig.setIncludeOperationalAttributes(true);
backend.exportLDIF(exportConfig);
}
String ldifString = ldifOutputContent.toString();
assertEquals(ldifString.contains(testBaseDN.toString()), true, "Export without rootDN");
assertEquals(ldifString.contains(searchDN.toString()), true, "Export without rootDN");
// Import wants the backend to be configured but not initialized. Finalizing resets the status.
assertTrue(backend.supports(BackendOperation.LDIF_IMPORT), "Import not supported");
backend.finalizeBackend();
ByteArrayInputStream ldifImportContent = new ByteArrayInputStream(ldifOutputContent.toByteArray());
ByteArrayOutputStream rejectedEntries = new ByteArrayOutputStream();
try (final LDIFImportConfig importConf = new LDIFImportConfig(ldifImportContent))
{
importConf.setInvokeImportPlugins(true);
importConf.setClearBackend(true);
importConf.writeRejectedEntries(rejectedEntries);
importConf.setIncludeBranches(Collections.singleton(testBaseDN));
importConf.setSkipDNValidation(true);
importConf.setThreadCount(0);
backend.importLDIF(importConf, DirectoryServer.getInstance().getServerContext());
}
assertEquals(rejectedEntries.size(), 0,
"No entries should be rejected. Content was:\n" + rejectedEntries.toString());
backend.openBackend();
assertEquals(backend.getEntryCount(), getTotalNumberOfLDIFEntries(), "Not enough entries in DIT.");
/** +1 for the testBaseDN itself */
assertEquals(backend.getNumberOfEntriesInBaseDN(testBaseDN), getTotalNumberOfLDIFEntries(),
"Not enough entries in DIT.");
assertEquals(backend.getNumberOfChildren(testBaseDN), 1,
"Not enough entries in DIT.");
/** -2 for baseDn and People entry */
assertEquals(backend.getNumberOfChildren(testBaseDN.child(DN.valueOf("ou=People"))), getTotalNumberOfLDIFEntries() - 2,
"Not enough entries in DIT.");
VerifyConfig config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCompleteIndex("dn2id");
for (String indexName : backendIndexes.keySet())
{
config.addCompleteIndex(indexName);
}
assertThat(backend.verifyBackend(config)).isEqualTo(0);
config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCleanIndex("dn2id");
for (String indexName : backendIndexes.keySet())
{
config.addCleanIndex(indexName);
}
assertThat(backend.verifyBackend(config)).isEqualTo(0);
}
@Test
public void testRebuildAllIndex() throws Exception
{
final EntryContainer entryContainer = backend.getRootContainer().getEntryContainers().iterator().next();
// Delete all the indexes
backend.getRootContainer().getStorage().write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
entryContainer.getDN2ID().delete(txn);
entryContainer.getDN2URI().delete(txn);
entryContainer.getID2ChildrenCount().delete(txn);
for(VLVIndex idx : entryContainer.getVLVIndexes())
{
idx.setTrusted(txn, false);
idx.delete(txn);
}
for(AttributeIndex attribute : entryContainer.getAttributeIndexes())
{
for(Index idx : attribute.getNameToIndexes().values())
{
idx.setTrusted(txn, false);
idx.delete(txn);
}
}
}
});
RebuildConfig rebuildConf = new RebuildConfig();
rebuildConf.setBaseDN(DN.valueOf("dc=test,dc=com"));
rebuildConf.setRebuildMode(RebuildMode.ALL);
backend.closeBackend();
backend.rebuildBackend(rebuildConf, DirectoryServer.getInstance().getServerContext());
backend.openBackend();
VerifyConfig config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCompleteIndex("dn2id");
for (String indexName : backendIndexes.keySet())
{
config.addCompleteIndex(indexName);
}
assertThat(backend.verifyBackend(config)).isEqualTo(0);
config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCleanIndex("dn2id");
for (String indexName : backendIndexes.keySet())
{
config.addCleanIndex(indexName);
}
assertThat(backend.verifyBackend(config)).isEqualTo(0);
}
@Test
public void testRebuildDegradedIndex() throws Exception
{
final EntryContainer entryContainer = backend.getRootContainer().getEntryContainers().iterator().next();
final Set<String> dirtyIndexes = new HashSet<>(Arrays.asList(new String[] { "sn", "uid", "telephoneNumber" }));
assertThat(backendIndexes.keySet()).containsAll(dirtyIndexes);
// Delete all the indexes
backend.getRootContainer().getStorage().write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
for(AttributeIndex attribute : entryContainer.getAttributeIndexes())
{
boolean trusted = !dirtyIndexes.contains(attribute.getAttributeType().getNameOrOID());
for(Index idx : attribute.getNameToIndexes().values())
{
idx.setTrusted(txn, trusted);
}
}
}
});
RebuildConfig rebuildConf = new RebuildConfig();
rebuildConf.setBaseDN(DN.valueOf("dc=test,dc=com"));
rebuildConf.setRebuildMode(RebuildMode.DEGRADED);
backend.closeBackend();
backend.rebuildBackend(rebuildConf, DirectoryServer.getInstance().getServerContext());
backend.openBackend();
VerifyConfig config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCompleteIndex("dn2id");
for (String indexName : backendIndexes.keySet())
{
config.addCompleteIndex(indexName);
}
assertThat(backend.verifyBackend(config)).isEqualTo(0);
config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCleanIndex("dn2id");
for (String indexName : backendIndexes.keySet())
{
config.addCleanIndex(indexName);
}
assertThat(backend.verifyBackend(config)).isEqualTo(0);
}
@Test
public void testVerifyID2ChildrenCount() throws Exception
{
final Storage storage = backend.getRootContainer().getStorage();
final DN2ID dn2ID = backend.getRootContainer().getEntryContainer(testBaseDN).getDN2ID();
final ID2ChildrenCount id2ChildrenCount = backend.getRootContainer().getEntryContainer(testBaseDN).getID2ChildrenCount();
final VerifyConfig config = new VerifyConfig();
config.setBaseDN(DN.valueOf("dc=test,dc=com"));
config.addCleanIndex("dn2id");
assertThat(backend.verifyBackend(config)).isEqualTo(0);
// Insert an error
final EntryID peopleID = storage.read(new ReadOperation<EntryID>()
{
@Override
public EntryID run(ReadableTransaction txn) throws Exception
{
return dn2ID.get(txn, testBaseDN.child(DN.valueOf("ou=People")));
}
});
storage.write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
final long delta = id2ChildrenCount.removeCount(txn, peopleID);
id2ChildrenCount.updateTotalCount(txn, -delta);
id2ChildrenCount.updateCount(txn, peopleID, 1);
id2ChildrenCount.updateTotalCount(txn, 1);
}
});
assertThat(backend.verifyBackend(config)).isEqualTo(1);
}
@Test
public void testBackupAndRestore() throws Exception
{
assertEquals(backend.supports(BackendOperation.BACKUP), true, "Skip Backup");
assertNotNull(backupID, "Need to setup a backupID");
final String backupPath = TestCaseUtils.createTemporaryDirectory("backup").getAbsolutePath();
backupDirectory = new BackupDirectory(backupPath, testBaseDN);
BackupConfig backupConf = new BackupConfig(backupDirectory, backupID, false);
backend.createBackup(backupConf);
assertTrue(backend.supports(BackendOperation.RESTORE), "Skip Restore");
backend.restoreBackup(new RestoreConfig(backupDirectory, backupID, true));
}
@Test(expectedExceptions=ReadOnlyStorageException.class)
public void testReadOnly() throws Exception
{
C backendCfg = createBackendCfg();
when(backendCfg.dn()).thenReturn(testBaseDN);
when(backendCfg.getBaseDN()).thenReturn(newTreeSet(testBaseDN));
when(backendCfg.listBackendIndexes()).thenReturn(new String[0]);
when(backendCfg.listBackendVLVIndexes()).thenReturn(new String[0]);
final Storage storage = backend.configureStorage(backendCfg, DirectoryServer.getInstance().getServerContext());
final RootContainer readOnlyContainer = new RootContainer(backend.getBackendID(), storage, backendCfg);
// Put backend offline so that export LDIF open read-only container
backend.finalizeBackend();
try
{
readOnlyContainer.open(AccessMode.READ_ONLY);
readOnlyContainer.getStorage().write(new WriteOperation()
{
@Override
public void run(WriteableTransaction txn) throws Exception
{
txn.put(new TreeName("dc=test,dc=com", "id2entry"), ByteString.valueOfUtf8("key"), ByteString.valueOfUtf8("value"));
}
});
}
finally
{
readOnlyContainer.close();
backend.openBackend();
}
}
}