/* * 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.replication.plugin; import static org.forgerock.opendj.ldap.ModificationType.*; import static org.mockito.Matchers.*; import static org.mockito.Mockito.*; import static org.opends.server.util.CollectionUtils.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Iterator; import java.util.List; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.ModificationType; import org.opends.server.TestCaseUtils; import org.opends.server.replication.ReplicationTestCase; import org.opends.server.replication.common.CSN; import org.opends.server.replication.protocol.ModifyContext; import org.opends.server.replication.protocol.OperationContext; import org.opends.server.types.Attribute; import org.opends.server.types.AttributeType; import org.opends.server.types.Attributes; import org.opends.server.types.DirectoryException; import org.opends.server.types.Entry; import org.opends.server.types.Modification; import org.opends.server.types.operation.PreOperationModifyOperation; import org.testng.Assert; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; /** * Tests the single valued attribute conflict resolution (part of modify replay). * <p> * It produces series of changes and replay them out of order by generating all possible * permutations. The goal is to end up in the same final state whatever the order. * <p> * The tests are built this way: * <ol> * <li>Start from an entry without (resp. with) an initial value for the targeted single valued * attribute</li> * <li>Replay a "seed" modify operation, that happened at time t1</li> * <li>Then replay any of all the possible operations that could come after, that happened at time * t2</li> * </ol> * All permutations for these sequence of operations are tested. * <p> * The test finally asserts the {@code ds-sync-hist} attribute always end in the same state whatever * the permutation. */ @SuppressWarnings("javadoc") public class ModifyReplaySingleValuedAttributeTest extends ReplicationTestCase { private static final String ATTRIBUTE_NAME = "displayName"; private static final String SYNCHIST = "ds-sync-hist"; private Entry entry; private static class Mod { private final int time; private final Modification modification; private Mod(ModificationType modType, int t) { this(modType, null, t); } private Mod(ModificationType modType, String value, int t) { this.modification = newModification(modType, value); this.time = t; } private PreOperationModifyOperation toOperation() { final ModifyContext value = new ModifyContext(new CSN(0, time, 0), null); PreOperationModifyOperation op = mock(PreOperationModifyOperation.class); when(op.getModifications()).thenReturn(newArrayList(modification)); when(op.getAttachment(eq(OperationContext.SYNCHROCONTEXT))).thenReturn(value); return op; } /** Implemented to get a nice display for each tests in Eclipse UI. */ @Override public String toString() { String modType = modification.getModificationType().toString().toUpperCase(); Iterator<ByteString> it = modification.getAttribute().iterator(); String attrValue = it.hasNext() ? "\"" + it.next().toString() + "\" " : ""; return modType + " " + attrValue + "t" + time; } } private static Object[][] generatePermutations(Object[][] scenarios) { List<Object[]> results = new ArrayList<Object[]>(); for (Object[] scenario : scenarios) { generate((Object[]) scenario[0], (Attribute) scenario[1], results); } return results.toArray(new Object[results.size()][]); } private static void generate(Object[] array, Attribute dsSyncHist, List<Object[]> results) { generate(array.length, array, dsSyncHist, results); } private static void generate(int n, Object[] array, Attribute dsSyncHist, List<Object[]> results) { if (n == 1) { results.add(new Object[] { Arrays.asList(Arrays.copyOf(array, array.length)), dsSyncHist, }); return; } for (int i = 0; i < n - 1; i += 1) { generate(n - 1, array, dsSyncHist, results); if (n % 2 == 0) { swap(array, i, n - 1); } else { swap(array, 0, n - 1); } } generate(n - 1, array, dsSyncHist, results); } private static <E> void swap(E[] array, int i, int j) { E tmp = array[i]; array[i] = array[j]; array[j] = tmp; } @DataProvider public Object[][] add_data() { // @formatter:off return generatePermutations(new Object[][] { { mods(new Mod(ADD, "X", 1)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(ADD, "X", 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(ADD, "X", 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(ADD, "X", 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(ADD, "X", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(ADD, "X", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(ADD, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(ADD, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(ADD, "X", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), } }); // @formatter:on } @DataProvider public Object[][] delete_noInitialValue_data() { // @formatter:off return generatePermutations(new Object[][] { { mods(new Mod(DELETE, "X", 1)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(DELETE, 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(DELETE, 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(DELETE, "Y", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(DELETE, 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(DELETE, 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, }); // @formatter:on } @DataProvider public Object[][] delete_initialValueX_data() { // @formatter:off return generatePermutations(new Object[][] { { mods(new Mod(DELETE, "X", 1)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(DELETE, "X", 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(DELETE, "X", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "Y", 1)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(ADD, "X", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(ADD, "Y", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(DELETE, "X", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(DELETE, "Y", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(DELETE, 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(DELETE, 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(DELETE, "Y", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(DELETE, 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(DELETE, 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(DELETE, 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, }); // @formatter:on } @DataProvider public Object[][] replace_noInitialValue_data() { // @formatter:off return generatePermutations(new Object[][] { { mods(new Mod(REPLACE, "X", 1)), dsSyncHist(1, ":repl:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(1, ":repl:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, 1)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(REPLACE, 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(REPLACE, 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(REPLACE, 1), new Mod(DELETE, "X", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(REPLACE, 1), new Mod(DELETE, "Y", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(REPLACE, 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(REPLACE, 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, }); // @formatter:on } @DataProvider public Object[][] replace_initialValueX_data() { // @formatter:off return generatePermutations(new Object[][] { { mods(new Mod(REPLACE, "X", 1)), dsSyncHist(1, ":repl:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "X", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(1, ":add:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(REPLACE, "X", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "Y", 1)), dsSyncHist(1, ":repl:Y"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(DELETE, "X", 2)), dsSyncHist(1, ":repl:Y"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(DELETE, "Y", 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(REPLACE, "Y", 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, 1)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(REPLACE, 1), new Mod(ADD, "X", 2)), dsSyncHist(2, ":add:X"), }, { mods(new Mod(REPLACE, 1), new Mod(ADD, "Y", 2)), dsSyncHist(2, ":add:Y"), }, { mods(new Mod(REPLACE, 1), new Mod(DELETE, "X", 2)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(REPLACE, 1), new Mod(DELETE, "Y", 2)), dsSyncHist(1, ":attrDel"), }, { mods(new Mod(REPLACE, 1), new Mod(DELETE, 2)), dsSyncHist(2, ":attrDel"), }, { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "X", 2)), dsSyncHist(2, ":repl:X"), }, { mods(new Mod(REPLACE, 1), new Mod(REPLACE, "Y", 2)), dsSyncHist(2, ":repl:Y"), }, { mods(new Mod(REPLACE, 1), new Mod(REPLACE, 2)), dsSyncHist(2, ":attrDel"), }, }); // @formatter:on } @Test(dataProvider = "add_data", enabled = false) public void add_noInitialValue(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception { noValue(); // also covers the initialValue("X"); case replay(mods, expectedDsSyncHist); } @Test(dataProvider = "delete_noInitialValue_data", enabled = false) public void delete_noInitialValue(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception { noValue(); replay(mods, expectedDsSyncHist); } @Test(dataProvider = "delete_initialValueX_data", enabled = false) public void delete_initialValueX(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception { initialValue("X"); replay(mods, expectedDsSyncHist); } @Test(dataProvider = "replace_noInitialValue_data", enabled = false) public void replace_noInitialValue(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception { noValue(); replay(mods, expectedDsSyncHist); } @Test(dataProvider = "replace_initialValueX_data", enabled = false) public void replace_initialValueX(List<Mod> mods, Attribute expectedDsSyncHist) throws Exception { initialValue("X"); replay(mods, expectedDsSyncHist); } @Test(enabled = false) public void diffEntries_addThenDel() throws Exception { initialValue("X"); replaySameTime(newArrayList(newModification(ADD, "Y"), newModification(DELETE, "X")), dsSyncHist(1, ":add:Y")); } @Test(enabled = false) public void diffEntries_delThenAdd() throws Exception { initialValue("X"); replaySameTime(newArrayList(newModification(DELETE, "X"), newModification(ADD, "Y")), dsSyncHist(1, ":add:Y")); } private void replaySameTime(List<Modification> mods, Attribute expectedDsSyncHist) throws DirectoryException { final ModifyContext value = new ModifyContext(new CSN(0, 1, 0), null); PreOperationModifyOperation op = mock(PreOperationModifyOperation.class); when(op.getModifications()).thenReturn(mods); when(op.getAttachment(eq(OperationContext.SYNCHROCONTEXT))).thenReturn(value); EntryHistorical entryHistorical = EntryHistorical.newInstanceFromEntry(entry); entryHistorical.replayOperation(op, entry); entry.applyModification(new Modification(REPLACE, entryHistorical.encodeAndPurge())); AttributeType attrType = expectedDsSyncHist.getAttributeType(); Attribute actual = entry.getExactAttribute(attrType, Collections.<String> emptySet()); Assert.assertEquals(actual, expectedDsSyncHist, "wrong final value for ds-sync-hist attribute"); } private void noValue() throws Exception { // @formatter:off entry = TestCaseUtils.makeEntry( "dn: uid=test.user", "objectClass: top", "objectClass: person", "objectClass: organizationalPerson", "objectClass: inetOrgPerson", "uid: test.user", "givenName: Test", "sn: User", "cn: Test User", "userPassword: password"); // @formatter:on } private void initialValue(String attrValue) throws Exception { noValue(); entry.applyModification(newModification(REPLACE, attrValue)); } private void replay(List<Mod> mods, Attribute expectedDsSyncHist) throws DirectoryException { for (Mod op : mods) { EntryHistorical entryHistorical = EntryHistorical.newInstanceFromEntry(entry); entryHistorical.replayOperation(op.toOperation(), entry); entry.applyModification(new Modification(REPLACE, entryHistorical.encodeAndPurge())); } AttributeType attrType = expectedDsSyncHist.getAttributeType(); Attribute actual = entry.getExactAttribute(attrType, Collections.<String> emptySet()); Assert.assertEquals(actual, expectedDsSyncHist, "wrong final value for ds-sync-hist attribute"); } private static Object[] mods(Mod... mods) { return mods; } private static Attribute dsSyncHist(int t, String partialDsSyncHist) { String value = ATTRIBUTE_NAME + ":000000000000000000000000000" + t + partialDsSyncHist; return Attributes.create(SYNCHIST, value); } private static Modification newModification(ModificationType modType, String value) { Attribute attr = value != null ? Attributes.create(ATTRIBUTE_NAME, value) : Attributes.empty(ATTRIBUTE_NAME); return new Modification(modType, attr); } }