From 26defc28efac7cc706352bea7126a19308065468 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 23 Sep 2026 07:41:33 +0000
Subject: [PATCH] [#1021] Reset the objectClass attribute of an entry when its object classes change (#1022)

---
 opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java |   73 ++++++++++++++++++++++++++++++++++++
 1 files changed, 73 insertions(+), 0 deletions(-)

diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java
index 633227b..396a2eb 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/plugin/ModifyConflictTest.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2006-2010 Sun Microsystems, Inc.
  * Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.replication.plugin;
 
@@ -69,6 +70,8 @@
 public class ModifyConflictTest extends ReplicationTestCase
 {
   private static final String ORGANIZATION = "organization";
+  private static final String OBJECTCLASS = "objectClass";
+  private static final String EXTENSIBLEOBJECT = "extensibleObject";
   private static final String DISPLAYNAME = "displayName";
   private static final String EMPLOYEENUMBER = "employeeNumber";
   private static final String DESCRIPTION = "description";
@@ -1042,6 +1045,76 @@
   }
 
   /**
+   * Test that a replayed modification of the objectClass attribute leaves the entry handing out
+   * the object classes it ends up with.
+   * <p>
+   * Solving the conflicts of a delete reads the entry, and it does so before the core applies the
+   * modifications to it. The read must not fix what the entry answers afterwards: the backend
+   * stores the object classes of the entry but indexes it through its objectClass attribute, so
+   * an attribute left behind by the modification silently takes the objectClass index out of step
+   * with the stored entry.
+   */
+  @Test
+  public void replayObjectClassAddAndDelete() throws Exception
+  {
+    Entry entry = initializeEntry();
+    EntryHistorical hist = EntryHistorical.newInstanceFromEntry(entry);
+
+    List<Modification> mods = newArrayList(
+        newModification(ADD, OBJECTCLASS, EXTENSIBLEOBJECT),
+        newModification(DELETE, OBJECTCLASS, ORGANIZATION));
+
+    // The conflict resolution, which reads the entry to solve the delete.
+    replayModifies(entry, hist, 10, mods);
+    assertThat(mods).hasSize(2);
+
+    // The core, which applies the modifications to the entry.
+    applyModificationsTheWayTheCoreDoes(entry, mods);
+
+    assertThat(entry.getObjectClasses().values()).containsOnly(EXTENSIBLEOBJECT);
+    assertThat(objectClassAttributeOf(entry)).containsOnly(EXTENSIBLEOBJECT);
+  }
+
+  /**
+   * Applies the modifications to the entry the way {@code LocalBackendModifyOperation} does, that
+   * is with {@code addAttribute()} and {@code removeAttribute()} rather than with
+   * {@code Entry.applyModifications()}.
+   */
+  private void applyModificationsTheWayTheCoreDoes(Entry entry, List<Modification> mods)
+  {
+    for (Modification mod : mods)
+    {
+      Attribute attr = mod.getAttribute();
+      switch (mod.getModificationType().asEnum())
+      {
+      case ADD:
+        entry.addAttribute(attr, new LinkedList<ByteString>());
+        break;
+      case DELETE:
+        entry.removeAttribute(attr, new LinkedList<ByteString>());
+        break;
+      default:
+        entry.replaceAttribute(attr);
+        break;
+      }
+    }
+  }
+
+  /** Returns the object classes of the entry as its objectClass attribute reports them. */
+  private List<String> objectClassAttributeOf(Entry entry)
+  {
+    List<String> values = new LinkedList<>();
+    for (Attribute attr : entry.getAllAttributes(getObjectClassAttributeType()))
+    {
+      for (ByteString value : attr)
+      {
+        values.add(value.toString());
+      }
+    }
+    return values;
+  }
+
+  /**
    * Check that the mods given as first parameter match the next parameters.
    *
    * @param mods The mods that must be tested.

--
Gitblit v1.10.0