From f85d2d244205d32a39ff5d5f52e90ce2e10fc24a Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 08 Sep 2026 07:29:00 +0000
Subject: [PATCH] [#910] Test that a ModifyDN conflict is still solved while server-error-result-code is one of the conflict codes (#938)
---
opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java | 87 +++++++++++++++++++++++++++++++++++++++++++
1 files changed, 86 insertions(+), 1 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java
index 7c66fb5..65c4498 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/NamingConflictTest.java
@@ -13,6 +13,7 @@
*
* Copyright 2009-2010 Sun Microsystems, Inc.
* Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.replication.plugin;
@@ -57,6 +58,17 @@
private TestSynchronousReplayQueue queue;
+ /**
+ * The result code to put back in {@code ds-cfg-server-error-result-code}, or
+ * {@code null} when this test did not change it.
+ * <p>
+ * The setting is server-wide, so it is put back by {@link #tearDown()} rather than by
+ * the test which changed it: a method the harness kills on its timeout, or interrupts
+ * inside a replay, would otherwise leave every later method of this class replaying
+ * against a result code it never asked for.
+ */
+ private Integer serverErrorResultCodeToRestore;
+
@BeforeMethod
public void setUpLocal() throws Exception
{
@@ -77,7 +89,19 @@
@AfterMethod
public void tearDown() throws Exception
{
- MultimasterReplication.deleteDomain(baseDN);
+ try
+ {
+ MultimasterReplication.deleteDomain(baseDN);
+ }
+ finally
+ {
+ if (serverErrorResultCodeToRestore != null)
+ {
+ final int resultCode = serverErrorResultCodeToRestore;
+ serverErrorResultCodeToRestore = null;
+ setServerErrorResultCode(resultCode);
+ }
+ }
}
/**
@@ -120,6 +144,67 @@
}
/**
+ * Test case for [Issue 910]: a naming conflict which
+ * {@code solveNamingConflict(ModifyDNOperation)} solves must still be solved while
+ * {@code ds-cfg-server-error-result-code} is set to one of the result codes conflict
+ * resolution owns.
+ * <p>
+ * That setting is a plain integer which is not validated as a result code, so it can be
+ * one of them. Here it is {@code UNWILLING_TO_PERFORM}, which is what a ModifyDN whose
+ * new superior is - on this replica - a subordinate of the entry being moved comes back
+ * with, and only conflict resolution can turn such a change into an operation which
+ * applies: it resolves both DNs again from the entryUUIDs the message carries. Reading
+ * the code as a failure of the server would take the change away from it - the message
+ * would never be rewritten, so no attempt would apply any better than the first - and
+ * the change would be retried in place, delivered again and finally given up on, with
+ * the entry left where it was.
+ * <p>
+ * {@code UpdateOperationTest.changeConflictResolutionCanNotSolveOnTheServerErrorCodeIsRetried}
+ * covers the other half: a change which fails with that same code and which conflict
+ * resolution can not solve is retried as the failure of the server it is.
+ */
+ @Test
+ public void modifyDnConflictIsSolvedWhileTheServerErrorCodeIsOneOfTheConflictCodes() throws Exception
+ {
+ final Entry entry = createAndAddEntry("modDnOnConflictingServerErrorCode");
+ final String entryUUID = getEntryUUID(entry.getName());
+
+ final Entry newParent = TestCaseUtils.addEntry(
+ "dn: ou=newParent," + TEST_ROOT_DN_STRING,
+ "objectClass: top",
+ "objectClass: organizationalUnit",
+ "ou: newParent");
+ final String newParentUUID = getEntryUUID(newParent.getName());
+
+ // Remembered for tearDown() rather than restored here: the code which is in force,
+ // which is the defined default unless the suite configured another one, and never a
+ // value hardcoded by this test.
+ serverErrorResultCodeToRestore =
+ getServerContext().getCoreConfigManager().getServerErrorResultCode().intValue();
+ setServerErrorResultCode(ResultCode.UNWILLING_TO_PERFORM.intValue());
+
+ /*
+ * The new superior as the master knew it: a DN which, here, is a subordinate of the
+ * entry being moved - as it would be after that parent was renamed on this replica.
+ * The operation reports UNWILLING_TO_PERFORM for as long as the message carries that
+ * DN - ERR_MODDN_NEW_SUPERIOR_IN_SUBTREE, and the memory backend answers the same on
+ * a new superior which is not in it - so the change is applied only if conflict
+ * resolution gets to rewrite the message with the DNs the entryUUIDs resolve to here.
+ */
+ final String staleNewSuperior = "ou=newParent," + entry.getName();
+ final CSN csn = gen.newCSN();
+ replayMsg(new ModifyDNMsg(entry.getName(), csn, entryUUID, newParentUUID, false,
+ staleNewSuperior, entry.getName().rdn().toString()));
+
+ final DN resolvedDN = newParent.getName().child(entry.getName().rdn());
+ assertTrue(entryExists(resolvedDN),
+ "the naming conflict was not solved: no entry at " + resolvedDN);
+ assertFalse(entryExists(entry.getName()), "the entry was not moved by the replayed ModifyDN");
+ assertTrue(domain.getServerState().cover(csn),
+ "a change which was applied must be recorded as replayed");
+ }
+
+ /**
* Test that when a previous conflict is resolved because
* a delete operation has removed one of the conflicting entries
* the other conflicting entry is correctly renamed to its original name.
--
Gitblit v1.10.0