/*
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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 2012-2013 ForgeRock AS
*/
package org.opends.server.extensions;
import static java.util.Collections.singleton;
import static java.util.Collections.singletonList;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
import static org.testng.Assert.assertTrue;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;
import org.opends.messages.Message;
import org.opends.messages.MessageBuilder;
import org.opends.server.TestCaseUtils;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.meta.EntityTagVirtualAttributeCfgDefn.ChecksumAlgorithm;
import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn;
import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn.ConflictBehavior;
import org.opends.server.admin.std.meta.VirtualAttributeCfgDefn.Scope;
import org.opends.server.admin.std.server.EntityTagVirtualAttributeCfg;
import org.opends.server.admin.std.server.VirtualAttributeCfg;
import org.opends.server.controls.LDAPAssertionRequestControl;
import org.opends.server.controls.LDAPPostReadRequestControl;
import org.opends.server.controls.LDAPPostReadResponseControl;
import org.opends.server.controls.LDAPPreReadRequestControl;
import org.opends.server.controls.LDAPPreReadResponseControl;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.SearchOperation;
import org.opends.server.core.SearchOperationWrapper;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.protocols.ldap.LDAPModification;
import org.opends.server.schema.DirectoryStringSyntax;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.AttributeValues;
import org.opends.server.types.ByteString;
import org.opends.server.types.ConditionResult;
import org.opends.server.types.Control;
import org.opends.server.types.DN;
import org.opends.server.types.DereferencePolicy;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import org.opends.server.types.ModificationType;
import org.opends.server.types.RawAttribute;
import org.opends.server.types.RawModification;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchFilter;
import org.opends.server.types.SearchScope;
import org.opends.server.types.VirtualAttributeRule;
import org.opends.server.util.StaticUtils;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
* A set of test cases for the entity tag virtual attribute provider.
*/
public class EntityTagVirtualAttributeProviderTestCase extends
ExtensionsTestCase
{
private static final String DESCRIPTION = "description";
private static final String ETAG = "etag";
private final AttributeValue dummyValue = AttributeValues.create(
ByteString.valueOf("dummy"), ByteString.valueOf("dummy"));
private final EntityTagVirtualAttributeProvider provider = new EntityTagVirtualAttributeProvider();
private boolean changeListenerRemoved = false;
private boolean changeListenerAdded = false;
private final EntityTagVirtualAttributeCfg config = new EntityTagVirtualAttributeCfg()
{
private final TreeSet<AttributeType> excludedAttributes = new TreeSet<AttributeType>();
public void addChangeListener(
final ConfigurationChangeListener<VirtualAttributeCfg> listener)
{
// Should not be called.
throw new IllegalStateException();
}
public void addEntityTagChangeListener(
final ConfigurationChangeListener<EntityTagVirtualAttributeCfg> listener)
{
changeListenerAdded = true;
}
public Class<? extends EntityTagVirtualAttributeCfg> configurationClass()
{
// Not needed.
return null;
}
public DN dn()
{
// Not needed.
return null;
}
public AttributeType getAttributeType()
{
// Not needed.
return null;
}
public SortedSet<DN> getBaseDN()
{
// Not needed.
return null;
}
public ChecksumAlgorithm getChecksumAlgorithm()
{
return ChecksumAlgorithm.ADLER_32;
}
public ConflictBehavior getConflictBehavior()
{
// Not needed.
return null;
}
public SortedSet<AttributeType> getExcludedAttribute()
{
return excludedAttributes;
}
public SortedSet<String> getFilter()
{
// Not needed.
return null;
}
public SortedSet<DN> getGroupDN()
{
// Not needed.
return null;
}
public String getJavaClass()
{
// Not needed.
return null;
}
public Scope getScope()
{
// Not needed.
return null;
}
public boolean isEnabled()
{
return true;
}
public void removeChangeListener(
final ConfigurationChangeListener<VirtualAttributeCfg> listener)
{
// Should not be called.
throw new IllegalStateException();
}
public void removeEntityTagChangeListener(
final ConfigurationChangeListener<EntityTagVirtualAttributeCfg> listener)
{
changeListenerRemoved = true;
}
};
/**
* Ensures that the Directory Server is running.
*
* @throws Exception
* If an unexpected problem occurs.
*/
@BeforeClass()
public void startServer() throws Exception
{
TestCaseUtils.startServer();
// Initialize the provider.
config.getExcludedAttribute().add(
DirectoryServer.getAttributeType("modifytimestamp"));
provider.initializeVirtualAttributeProvider(config);
}
/**
* Tests that approximate matching is not supported.
*/
@Test
public void testApproximatelyEqualTo()
{
assertEquals(provider.approximatelyEqualTo(null, null, null),
ConditionResult.UNDEFINED);
}
/**
* Tests that finalization removes the change listener.
*/
@Test
public void testFinalizeVirtualAttributeProvider()
{
provider.finalizeVirtualAttributeProvider();
assertTrue(changeListenerRemoved);
}
/**
* Tests the getValues method returns an ETag whose value represents a 64-bit
* non-zero long encoded as hex.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testGetValuesBasic() throws Exception
{
final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
getEntityTag(e);
}
/**
* Tests the getValues method returns a different value for entries which are
* different.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testGetValuesDifferent() throws Exception
{
final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example1,dc=com",
"objectClass: top", "objectClass: domain", "dc: example1");
final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example2,dc=com",
"objectClass: top", "objectClass: domain", "dc: example2");
assertFalse(getEntityTag(e1).equals(getEntityTag(e2)));
}
/**
* Tests the getValues method ignores excluded attributes.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testGetValuesIgnoresExcludedAttributes() throws Exception
{
final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example",
"modifyTimestamp: 20120222232918Z");
assertEquals(getEntityTag(e1), getEntityTag(e2));
}
/**
* Tests the getValues method returns the same value for entries having the
* same content but with attributes in a different order.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testGetValuesNormalizedOrder() throws Exception
{
final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "description: one",
"description: two", "dc: example");
final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example",
"description: two", "description: one");
assertEquals(getEntityTag(e1), getEntityTag(e2));
}
/**
* Tests the getValues method returns the same value for different instances
* of the same entry.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testGetValuesRepeatable() throws Exception
{
final Entry e1 = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
final Entry e2 = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
assertEquals(getEntityTag(e1), getEntityTag(e2));
}
/**
* Tests that ordering matching is not supported.
*/
@Test
public void testGreaterThanOrEqualTo()
{
assertEquals(provider.greaterThanOrEqualTo(null, null, null),
ConditionResult.UNDEFINED);
}
/**
* Tests hasAllValues() membership.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testHasAllValues() throws Exception
{
final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
final AttributeValue value = getEntityTag(e);
assertTrue(provider.hasAllValues(e, null,
Collections.<AttributeValue> emptySet()));
assertTrue(provider.hasAllValues(e, null, Collections.singleton(value)));
assertFalse(provider.hasAllValues(e, null,
Collections.singleton(dummyValue)));
assertFalse(provider
.hasAllValues(e, null, Arrays.asList(value, dummyValue)));
}
/**
* Tests hasAnyValues() membership.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testHasAnyValue() throws Exception
{
final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
final AttributeValue value = getEntityTag(e);
assertTrue(provider.hasAnyValue(e, null, Collections.singleton(value)));
assertFalse(provider
.hasAnyValue(e, null, Collections.singleton(dummyValue)));
assertTrue(provider.hasAnyValue(e, null, Arrays.asList(value, dummyValue)));
}
/**
* Tests that the etags are always present.
*/
@Test
public void testHasValue1()
{
assertTrue(provider.hasValue(null, null));
}
/**
* Tests testHasValue membership.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testHasValue2() throws Exception
{
final Entry e = TestCaseUtils.makeEntry("dn: dc=example,dc=com",
"objectClass: top", "objectClass: domain", "dc: example");
final AttributeValue value = getEntityTag(e);
assertTrue(provider.hasValue(e, null, value));
assertFalse(provider.hasValue(e, null, dummyValue));
}
/**
* Tests that initialization adds the change listener.
*/
@Test
public void testInitializeVirtualAttributeProvider()
{
// This was actually done during initialization of this test. Check that the
// listener was registered.
assertTrue(changeListenerAdded);
}
/**
* Tests that isConfigurationAcceptable always returns true.
*/
@Test
public void testIsConfigurationAcceptable()
{
assertTrue(provider.isConfigurationAcceptable(config, null));
}
/**
* Tests that the etags are single-valued.
*/
@Test
public void testIsMultiValued()
{
assertFalse(provider.isMultiValued());
}
/**
* Tests that searching based on etag filters is not supported.
*/
@Test
public void testIsSearchable()
{
assertFalse(provider.isSearchable(null, null, false));
assertFalse(provider.isSearchable(null, null, true));
}
/**
* Tests that ordering matching is not supported.
*/
@Test
public void testLessThanOrEqualTo()
{
assertEquals(provider.lessThanOrEqualTo(null, null, null),
ConditionResult.UNDEFINED);
}
/**
* Tests that substring matching is not supported.
*/
@Test
public void testMatchesSubstring()
{
assertEquals(provider.matchesSubstring(null, null, null, null, null),
ConditionResult.UNDEFINED);
}
/**
* Tests that searching based on etag filters is not supported.
*/
@Test
public void testProcessSearch()
{
final SearchOperation search = new SearchOperationWrapper(null)
{
ResultCode resultCode = null;
MessageBuilder message = null;
/**
* {@inheritDoc}
*/
public void appendErrorMessage(final Message message)
{
this.message = new MessageBuilder(message);
}
/**
* {@inheritDoc}
*/
public MessageBuilder getErrorMessage()
{
return message;
}
/**
* @return the resultCode
*/
public ResultCode getResultCode()
{
return resultCode;
}
/**
* {@inheritDoc}
*/
public void setResultCode(final ResultCode resultCode)
{
this.resultCode = resultCode;
}
};
VirtualAttributeRule rule = new VirtualAttributeRule(
DirectoryServer.getAttributeType(ETAG), provider,
Collections.<DN> emptySet(), SearchScope.WHOLE_SUBTREE,
Collections.<DN> emptySet(), Collections.<SearchFilter> emptySet(),
VirtualAttributeCfgDefn.ConflictBehavior.REAL_OVERRIDES_VIRTUAL);
provider.processSearch(rule, search);
assertEquals(search.getResultCode(), ResultCode.UNWILLING_TO_PERFORM);
assertNotNull(search.getErrorMessage());
}
/**
* Simulates the main use case for entity tag support: optimistic concurrency.
* <p>
* This test reads an entry requesting its etag, then performs an update using
* an assertion control to prevent the change from being applied if the etag
* has changed since the read was performed.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testOptimisticConcurrency() throws Exception
{
// Use an internal connection.
AttributeType etagType = DirectoryServer.getAttributeType(ETAG);
AttributeType descrType = DirectoryServer.getAttributeType(DESCRIPTION);
String userDN = "uid=test.user,ou=People,o=test";
InternalClientConnection conn = InternalClientConnection
.getRootConnection();
// Create a test backend containing the user entry to be modified.
TestCaseUtils.initializeTestBackend(true);
// @formatter:off
TestCaseUtils.addEntries(
"dn: ou=People,o=test",
"objectClass: top",
"objectClass: organizationalUnit",
"ou: People",
"",
"dn: uid=test.user,ou=People,o=test",
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: test.user",
"givenName: Test",
"sn: User",
"cn: Test User",
"userPassword: password");
// @formatter:on
// Read the user entry and get the etag.
Entry e1 = readEntry(conn, userDN);
String etag1 = e1
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag1);
// Apply a change using the assertion control for optimistic concurrency.
List<RawModification> mods = Collections
.<RawModification> singletonList(new LDAPModification(
ModificationType.REPLACE, RawAttribute.create(DESCRIPTION,
"first modify")));
List<Control> ctrls = Collections
.<Control> singletonList(new LDAPAssertionRequestControl(true,
LDAPFilter.createEqualityFilter(ETAG, ByteString.valueOf(etag1))));
ModifyOperation modifyOperation = conn.processModify(userDN, mods, ctrls);
assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
// Reread the entry and check that the description has been added and that
// the etag has changed.
Entry e2 = readEntry(conn, userDN);
String etag2 = e2
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag2);
assertFalse(etag1.equals(etag2));
String description2 = e2.getAttributeValue(descrType,
DirectoryStringSyntax.DECODER);
assertNotNull(description2);
assertEquals(description2, "first modify");
// Simulate a concurrent update: perform another update using the old etag.
mods = Collections.<RawModification> singletonList(new LDAPModification(
ModificationType.REPLACE, RawAttribute.create(DESCRIPTION,
"second modify")));
modifyOperation = conn.processModify(userDN, mods, ctrls);
assertEquals(modifyOperation.getResultCode(), ResultCode.ASSERTION_FAILED);
// Reread the entry and check that the description and etag have not
// changed.
Entry e3 = readEntry(conn, userDN);
String etag3 = e3
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag3);
assertEquals(etag2, etag3);
String description3 = e3.getAttributeValue(descrType,
DirectoryStringSyntax.DECODER);
assertNotNull(description3);
assertEquals(description3, description2);
}
/**
* Tests that the etag returned with a pre-read control after a modify
* operation is correct. See OPENDJ-861.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testPreReadControl() throws Exception
{
AttributeType etagType = DirectoryServer.getAttributeType(ETAG);
AttributeType descrType = DirectoryServer.getAttributeType(DESCRIPTION);
String userDN = "uid=test.user,ou=People,o=test";
// Use an internal connection.
InternalClientConnection conn = InternalClientConnection
.getRootConnection();
// Create a test backend containing the user entry to be modified.
TestCaseUtils.initializeTestBackend(true);
// @formatter:off
TestCaseUtils.addEntries(
"dn: ou=People,o=test",
"objectClass: top",
"objectClass: organizationalUnit",
"ou: People",
"",
"dn: uid=test.user,ou=People,o=test",
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: test.user",
"givenName: Test",
"sn: User",
"cn: Test User",
"userPassword: password",
"description: initial value");
// @formatter:on
// Read the user entry and get the etag.
Entry e1 = readEntry(conn, userDN);
String etag1 = e1
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag1);
// Apply a change using the pre and post read controls.
List<RawModification> mods = Collections
.<RawModification> singletonList(new LDAPModification(
ModificationType.REPLACE, RawAttribute.create(DESCRIPTION,
"modified value")));
List<Control> ctrls = singletonList((Control) new LDAPPreReadRequestControl(
true, singleton(ETAG)));
ModifyOperation modifyOperation = conn.processModify(userDN, mods, ctrls);
assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
// Reread the entry and check that the description has been added and that
// the etag has changed.
Entry e2 = readEntry(conn, userDN);
String etag2 = e2
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag2);
assertFalse(etag1.equals(etag2));
String description2 = e2.getAttributeValue(descrType,
DirectoryStringSyntax.DECODER);
assertNotNull(description2);
assertEquals(description2, "modified value");
// Now check that the pre-read is the same as the initial etag.
LDAPPreReadResponseControl preReadControl = null;
for (Control control : modifyOperation.getResponseControls())
{
if (control instanceof LDAPPreReadResponseControl)
{
preReadControl = (LDAPPreReadResponseControl) control;
break;
}
}
assertNotNull(preReadControl);
String etagPreRead = preReadControl.getSearchEntry().getAttributeValue(
etagType, DirectoryStringSyntax.DECODER);
assertEquals(etagPreRead, etag1);
}
/**
* Tests that the etag returned with a post-read control after a modify
* operation is correct. See OPENDJ-861.
*
* @throws Exception
* If an unexpected exception occurred.
*/
@Test
public void testPostReadControl() throws Exception
{
AttributeType etagType = DirectoryServer.getAttributeType(ETAG);
AttributeType descrType = DirectoryServer.getAttributeType(DESCRIPTION);
String userDN = "uid=test.user,ou=People,o=test";
// Use an internal connection.
InternalClientConnection conn = InternalClientConnection
.getRootConnection();
// Create a test backend containing the user entry to be modified.
TestCaseUtils.initializeTestBackend(true);
// @formatter:off
TestCaseUtils.addEntries(
"dn: ou=People,o=test",
"objectClass: top",
"objectClass: organizationalUnit",
"ou: People",
"",
"dn: uid=test.user,ou=People,o=test",
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: test.user",
"givenName: Test",
"sn: User",
"cn: Test User",
"userPassword: password",
"description: initial value");
// @formatter:on
// Read the user entry and get the etag.
Entry e1 = readEntry(conn, userDN);
String etag1 = e1
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag1);
// Apply a change using the pre and post read controls.
List<RawModification> mods = Collections
.<RawModification> singletonList(new LDAPModification(
ModificationType.REPLACE, RawAttribute.create(DESCRIPTION,
"modified value")));
List<Control> ctrls = singletonList((Control) new LDAPPostReadRequestControl(
true, singleton(ETAG)));
ModifyOperation modifyOperation = conn.processModify(userDN, mods, ctrls);
assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
// Reread the entry and check that the description has been added and that
// the etag has changed.
Entry e2 = readEntry(conn, userDN);
String etag2 = e2
.getAttributeValue(etagType, DirectoryStringSyntax.DECODER);
assertNotNull(etag2);
assertFalse(etag1.equals(etag2));
String description2 = e2.getAttributeValue(descrType,
DirectoryStringSyntax.DECODER);
assertNotNull(description2);
assertEquals(description2, "modified value");
// Now check that the post-read is the same as the initial etag.
LDAPPostReadResponseControl postReadControl = null;
for (Control control : modifyOperation.getResponseControls())
{
if (control instanceof LDAPPostReadResponseControl)
{
postReadControl = (LDAPPostReadResponseControl) control;
break;
}
}
assertNotNull(postReadControl);
String etagPostRead = postReadControl.getSearchEntry().getAttributeValue(
etagType, DirectoryStringSyntax.DECODER);
assertEquals(etagPostRead, etag2);
}
private Entry readEntry(InternalClientConnection conn, String userDN)
throws DirectoryException
{
LinkedHashSet<String> attrList = new LinkedHashSet<String>(2);
attrList.add("*");
attrList.add(ETAG);
InternalSearchOperation searchOperation = conn.processSearch(userDN,
SearchScope.BASE_OBJECT, DereferencePolicy.NEVER_DEREF_ALIASES, 0, 0,
false, "(objectClass=*)", attrList);
assertEquals(searchOperation.getResultCode(), ResultCode.SUCCESS);
assertEquals(searchOperation.getSearchEntries().size(), 1);
Entry e = searchOperation.getSearchEntries().get(0);
assertNotNull(e);
return e;
}
private AttributeValue getEntityTag(final Entry e)
{
final Set<AttributeValue> values = provider.getValues(e, null);
assertEquals(values.size(), 1);
final AttributeValue value = values.iterator().next();
final ByteString bs = value.getValue();
assertEquals(bs.length(), 16);
boolean gotNonZeroByte = false;
for (int i = 0; i < 16; i++)
{
assertTrue(StaticUtils.isHexDigit(bs.byteAt(i)));
if (bs.byteAt(i) != 0x30)
{
gotNonZeroByte = true;
}
}
assertTrue(gotNonZeroByte);
return value;
}
}