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