From cae292bd84d7397e7ebc97ca7bd471154b7f18b9 Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Wed, 29 Jul 2026 14:31:11 +0000
Subject: [PATCH] Fix java/equals-on-unrelated-types CodeQL alerts (#781)

---
 opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java               |    3 
 opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java  |    3 
 opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java              |   67 +++++-
 opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java          |   26 ++
 opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanelTestCase.java      |  181 ++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java         |   35 ++-
 opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java              |   45 +++
 opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanelTestCase.java      |   69 ++++++
 opendj-server-legacy/src/test/java/org/opends/server/util/cli/LDAPConnectionConsoleInteractionTestCase.java  |   59 +++++
 opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java                |    3 
 opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanelTestCase.java |   72 +++++++
 11 files changed, 524 insertions(+), 39 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java
index 7fe9fee..d4808b9 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java
+++ b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/browser/NodeRefresher.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008-2010 Sun Microsystems, Inc.
  * Portions Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.guitools.controlpanel.browser;
 
@@ -460,7 +461,7 @@
         }
         conn = connectionPool.getConnection(url);
         remoteDn = DN.valueOf(url.getRawBaseDN());
-        if (remoteDn == null || "".equals(remoteDn))
+        if (remoteDn.isRootDN())
         {
           /* The referral has not a target DN specified: we
              have to use the DN of the entry that contains the
diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java
index 365e954..09cc4e2 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java
+++ b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/task/DeleteEntryTask.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008-2010 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.guitools.controlpanel.task;
 
@@ -315,7 +316,7 @@
       while (entryDNs.hasNext())
       {
         SearchResultEntry sr = entryDNs.readEntry();
-        if (!sr.getName().equals(""))
+        if (!sr.getName().isRootDN())
         {
           deleteSubtreeRecursively(conn, sr.getName(), null, toNotify);
         }
diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java
index a48c9f2..4c85b85 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java
+++ b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanel.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2009-2010 Sun Microsystems, Inc.
  * Portions Copyright 2012-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 
 package org.opends.guitools.controlpanel.ui;
@@ -43,7 +44,10 @@
 import org.forgerock.opendj.ldap.AttributeDescription;
 import org.forgerock.opendj.ldap.ByteString;
 import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.DecodeException;
 import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.schema.MatchingRule;
+import org.forgerock.opendj.ldap.schema.Schema;
 import org.opends.guitools.controlpanel.browser.BrowserController;
 import org.opends.guitools.controlpanel.browser.ConnectionWithControls;
 import org.opends.guitools.controlpanel.ui.nodes.BasicNode;
@@ -342,33 +346,48 @@
   @Override
   protected String getLDIF()
   {
-    String dn = this.dn.getText();
+    return getLDIF(entryToDuplicate, dn.getText(), rdnAttribute, new String(password.getPassword()),
+        getInfo().getServerDescriptor().getSchema());
+  }
+
+  /**
+   * Returns the LDIF representation of the duplicated entry.
+   *
+   * @param entryToDuplicate the entry that is being duplicated.
+   * @param dn the DN of the new entry.
+   * @param rdnAttribute the name of the attribute used in the RDN of the entry being duplicated.
+   * @param password the password typed by the user, empty if the user typed none.
+   * @param schema the schema of the server, {@code null} if it could not be read.
+   * @return the LDIF representation of the duplicated entry.
+   */
+  static String getLDIF(Entry entryToDuplicate, String dn, String rdnAttribute, String password, Schema schema)
+  {
     StringBuilder sb = new StringBuilder();
     sb.append("dn: ").append(dn);
     for (Attribute attr : entryToDuplicate.getAllAttributes())
     {
       AttributeDescription attrDesc = attr.getAttributeDescription();
       String attrName = attr.getAttributeDescriptionAsString();
-      if (attrDesc.equals(getUserPasswordAttributeType()))
+      if (attrDesc.getAttributeType().equals(getUserPasswordAttributeType()))
       {
-        sb.append("\n");
-        String pwd = new String(password.getPassword());
-        if (!pwd.isEmpty())
+        // The password is optional: the original password must not be copied and no line at all
+        // must be written when the user typed none.  Writing an empty line would terminate the
+        // LDIF record and silently drop every attribute that comes after this one.
+        if (!password.isEmpty())
         {
-          sb.append(attrName).append(": ").append(pwd);
+          sb.append("\n").append(attrName).append(": ").append(password);
         }
       }
       else if (!attrName.equalsIgnoreCase(rdnAttribute))
       {
-        if (!ViewEntryPanel.isEditable(attrDesc,
-            getInfo().getServerDescriptor().getSchema()))
+        if (!ViewEntryPanel.isEditable(attrDesc, schema))
         {
           continue;
         }
         for (ByteString value : attr)
         {
           sb.append("\n");
-          if (isBinary(attrName))
+          if (Utilities.hasBinarySyntax(attrName, schema))
           {
             sb.append(attrName).append(":: ").append(value.toBase64String());
           }
@@ -392,7 +411,7 @@
           for (ByteString value : attr)
           {
             sb.append("\n");
-            if (oldValue.equals(value))
+            if (isRDNValue(attrDesc, value, oldValue))
             {
               sb.append(attrName).append(": ").append(newValue);
             }
@@ -407,7 +426,33 @@
     return sb.toString();
   }
 
-  private String getFirstValue(DN dn)
+  /**
+   * Returns whether the provided value is the value the RDN of the entry being duplicated asserts.
+   * <p>
+   * The attributes used in an RDN usually have a case insensitive equality matching rule
+   * ({@code cn}, {@code ou} or {@code uid} for instance) and the value stored in the entry may
+   * therefore differ from the value written in the DN.  The comparison must consequently be done
+   * through the equality matching rule of the attribute and not with {@link String#equals(Object)}.
+   */
+  private static boolean isRDNValue(AttributeDescription attrDesc, ByteString value, String rdnValue)
+  {
+    MatchingRule equalityMatchingRule = attrDesc.getAttributeType().getEqualityMatchingRule();
+    if (equalityMatchingRule != null)
+    {
+      try
+      {
+        return equalityMatchingRule.normalizeAttributeValue(value)
+            .equals(equalityMatchingRule.normalizeAttributeValue(ByteString.valueOfUtf8(rdnValue)));
+      }
+      catch (DecodeException e)
+      {
+        // The values cannot be normalized: fall back to the case insensitive comparison below.
+      }
+    }
+    return rdnValue.equalsIgnoreCase(value.toString());
+  }
+
+  private static String getFirstValue(DN dn)
   {
     return dn.rdn().getFirstAVA().getAttributeValue().toString();
   }
diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java
index 7943416..a148565 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java
+++ b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanel.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008-2010 Sun Microsystems, Inc.
  * Portions Copyright 2013-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.guitools.controlpanel.ui;
 
@@ -571,15 +572,7 @@
   {
     if (OBJECTCLASS_ATTRIBUTE_TYPE_NAME.equalsIgnoreCase(attr))
     {
-      int nOcs = 0;
-      for (ByteString v : values)
-      {
-        if (!"top".equals(v))
-        {
-          nOcs++;
-        }
-      }
-      return nOcs > 1 ? GridBagConstraints.NORTHWEST : GridBagConstraints.WEST;
+      return countObjectClassesBesidesTop(values) > 1 ? GridBagConstraints.NORTHWEST : GridBagConstraints.WEST;
     }
     else if (isSingleValue(attr))
     {
@@ -595,6 +588,26 @@
     }
   }
 
+  /**
+   * Returns the number of object classes of an entry, not counting {@code top}.
+   *
+   * @param values the values of the object class attribute of the entry.
+   * @return the number of object classes, not counting {@code top}.
+   */
+  static int countObjectClassesBesidesTop(List<ByteString> values)
+  {
+    int nOcs = 0;
+    for (ByteString v : values)
+    {
+      // Object class values are case insensitive.
+      if (!OC_TOP.equalsIgnoreCase(v.toString()))
+      {
+        nOcs++;
+      }
+    }
+    return nOcs;
+  }
+
   private int anchor1(List<ByteString> values)
   {
     int size = values.size();
@@ -693,7 +706,7 @@
     }
     // Handle the root entry separately: most of its attributes are operational
     // so we filter a list of hardcoded attributes.
-    boolean isRootEntry = "".equals(sr.getName());
+    boolean isRootEntry = sr.getName().isRootDN();
     Schema schema = getInfo().getServerDescriptor().getSchema();
     if (isRootEntry)
     {
@@ -984,7 +997,7 @@
       else if (isConfirmPassword(attrName) || isPassword(attrName))
       {
         JPasswordField pf = Utilities.createPasswordField();
-        if (!"".equals(v))
+        if (!v.isEmpty())
         {
           pf.setText(getPasswordStringValue(getAttributeForConfirmPasswordKey(attrName), v));
         }
diff --git a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java
index d60833c..5afed14 100644
--- a/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java
+++ b/opendj-server-legacy/src/main/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanel.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008-2010 Sun Microsystems, Inc.
  * Portions Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.guitools.controlpanel.ui;
 
@@ -383,6 +384,37 @@
     return tableModel.getValues(attrName);
   }
 
+  /**
+   * Returns whether the provided attribute must be displayed as an {@link ObjectClassValue}
+   * instead of as a plain list of values.
+   * <p>
+   * The object class descriptor cannot be built without the schema, so when the schema could not
+   * be read the raw values must be displayed instead: otherwise the object class attribute would
+   * not be displayed at all.
+   *
+   * @param attrDesc the description of the attribute.
+   * @param schema the schema of the server, {@code null} if it could not be read.
+   * @return {@code true} if the attribute must be displayed as an {@link ObjectClassValue},
+   *         {@code false} otherwise.
+   */
+  static boolean isObjectClassWithSchema(AttributeDescription attrDesc, Schema schema)
+  {
+    return schema != null && attrDesc.getAttributeType().equals(getObjectClassAttributeType());
+  }
+
+  /**
+   * Returns whether the provided attribute is one of the required attributes of the entry.
+   *
+   * @param requiredAttrs the names of the required attributes, in lower case.
+   * @param attrDesc the description of the attribute.
+   * @return {@code true} if the attribute is required, {@code false} otherwise.
+   */
+  static boolean isRequired(Set<String> requiredAttrs, AttributeDescription attrDesc)
+  {
+    // The required attributes are stored in lower case since attribute names are case insensitive.
+    return requiredAttrs.contains(attrDesc.getNameOrOID().toLowerCase());
+  }
+
   /** The table model used by the tree in the panel. */
   private class LDAPEntryTableModel extends SortableTableModel
   implements Comparator<AttributeValuePair>
@@ -596,14 +628,11 @@
       for (Attribute attr : searchResult.getAllAttributes())
       {
         AttributeDescription attrDesc = attr.getAttributeDescription();
-        if (attrDesc.equals(getObjectClassAttributeType()))
+        if (isObjectClassWithSchema(attrDesc, schema))
         {
-          if (schema != null)
-          {
-            ocs = attr;
-            ObjectClassValue ocValue = getObjectClassDescriptor(ocs, schema);
-            allSortedValues.add(new AttributeValuePair(attrDesc, ocValue));
-          }
+          ocs = attr;
+          ObjectClassValue ocValue = getObjectClassDescriptor(ocs, schema);
+          allSortedValues.add(new AttributeValuePair(attrDesc, ocValue));
         }
         else
         {
@@ -781,7 +810,7 @@
 
     private boolean isRequired(AttributeValuePair value)
     {
-      return requiredAttrs.contains(value.attrDesc.getNameOrOID());
+      return TableViewEntryPanel.isRequired(requiredAttrs, value.attrDesc);
     }
 
     private boolean hasValue(AttributeValuePair value)
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
index 2ea1b35..cc09070 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/server/changelog/file/FileChangelogDB.java
@@ -12,6 +12,7 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2014-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.replication.server.changelog.file;
 
@@ -983,7 +984,7 @@
 
     private void tracePurgeDetails(final CSN purgeCSN, final CSN oldestNotPurgedCSN, final long sleepTime)
     {
-      if (purgeCSN.equals(oldestNotPurgedCSN.toStringUI()))
+      if (purgeCSN.equals(oldestNotPurgedCSN))
       {
         logger.trace("Purged up to %s. "
             + "now sleeping until next purge during %s",
diff --git a/opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java b/opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java
index 8282adf..35cd4ae 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/util/cli/LDAPConnectionConsoleInteraction.java
@@ -13,6 +13,7 @@
  *
  * Copyright 2008-2010 Sun Microsystems, Inc.
  * Portions Copyright 2011-2016 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.util.cli;
 
@@ -1603,12 +1604,10 @@
     {
       logger.warn(ERROR_CERTIFICATE_NULL_AUTH_TYPE.get());
     }
-    else
-    {
-      app.println(ApplicationTrustManager.Cause.NOT_TRUSTED.equals(authType)
-          ? INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(host, port)
-          : INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(host, port, host, host, port));
-    }
+    // The explanation only depends on the cause of the rejection, so it must be displayed even
+    // when the authentication type is unknown: otherwise the user is prompted to accept a
+    // certificate without being told what is wrong with it.
+    app.println(getCertificateRejectionMessage(cause, host, port));
 
     final X509Certificate[] chain = usedTrustManager.getLastRefusedChain();
     if (chain == null)
@@ -1625,6 +1624,21 @@
   }
 
   /**
+   * Returns the message explaining why the certificate presented by the server was rejected.
+   *
+   * @param cause the reason why the certificate was rejected.
+   * @param host the host name of the server.
+   * @param port the port of the server.
+   * @return the message explaining why the certificate was rejected.
+   */
+  static LocalizableMessage getCertificateRejectionMessage(ApplicationTrustManager.Cause cause, String host, int port)
+  {
+    return cause == ApplicationTrustManager.Cause.NOT_TRUSTED
+        ? INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(host, port)
+        : INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(host, port, host, host, port);
+  }
+
+  /**
    * Sets the heading that is displayed in interactive mode.
    *
    * @param heading
diff --git a/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanelTestCase.java b/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanelTestCase.java
new file mode 100644
index 0000000..446418e
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/DuplicateEntryPanelTestCase.java
@@ -0,0 +1,181 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.guitools.controlpanel.ui;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.Attribute;
+import org.forgerock.opendj.ldap.ByteString;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.opendj.ldap.Entry;
+import org.forgerock.opendj.ldap.LinkedHashMapEntry;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.forgerock.opendj.ldif.LDIFEntryReader;
+import org.opends.server.DirectoryServerTestCase;
+import org.testng.annotations.Test;
+
+/** Tests the LDIF generated by {@link DuplicateEntryPanel}. */
+@SuppressWarnings("javadoc")
+public class DuplicateEntryPanelTestCase extends DirectoryServerTestCase
+{
+  private static final Schema SCHEMA = Schema.getCoreSchema();
+  private static final String NEW_DN = "uid=user.2,ou=people,dc=example,dc=com";
+
+  /** An entry whose RDN attribute is single valued and which holds a password. */
+  private static Entry userEntry()
+  {
+    return new LinkedHashMapEntry(
+        "dn: uid=user.1,ou=people,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "objectClass: organizationalPerson",
+        "objectClass: inetOrgPerson",
+        "uid: user.1",
+        "userPassword: {SSHA}originalHashOfTheEntryBeingDuplicated",
+        "cn: User 1",
+        "sn: One");
+  }
+
+  /**
+   * When the user types no password, the original password must not be copied and no empty line
+   * must be written: an empty line terminates the LDIF record and every attribute written after
+   * the password would be silently dropped.
+   */
+  @Test
+  public void testEmptyPasswordDoesNotTruncateTheEntry() throws Exception
+  {
+    String ldif = DuplicateEntryPanel.getLDIF(userEntry(), NEW_DN, "uid", "", SCHEMA);
+
+    assertThat(ldif).doesNotContain("\n\n");
+    Entry duplicate = readEntry(ldif);
+    assertThat((Object) duplicate.getName()).isEqualTo(DN.valueOf(NEW_DN));
+    assertThat(valuesOf(duplicate, "userPassword")).isEmpty();
+    assertThat(valuesOf(duplicate, "objectClass"))
+        .containsExactly("top", "person", "organizationalPerson", "inetOrgPerson");
+    assertThat(valuesOf(duplicate, "uid")).containsExactly("user.2");
+    assertThat(valuesOf(duplicate, "cn")).containsExactly("User 1");
+    assertThat(valuesOf(duplicate, "sn")).containsExactly("One");
+  }
+
+  /** The password of the duplicated entry is the one typed by the user, not the original hash. */
+  @Test
+  public void testTypedPasswordReplacesTheOriginalOne() throws Exception
+  {
+    String ldif = DuplicateEntryPanel.getLDIF(userEntry(), NEW_DN, "uid", "typedPassword", SCHEMA);
+
+    Entry duplicate = readEntry(ldif);
+    assertThat(valuesOf(duplicate, "userPassword")).containsExactly("typedPassword");
+    assertThat(valuesOf(duplicate, "sn")).containsExactly("One");
+  }
+
+  /** The value of the RDN of the new entry replaces the value of the RDN of the original entry. */
+  @Test
+  public void testSingleValuedRDNAttributeUsesTheNewValue() throws Exception
+  {
+    String ldif = DuplicateEntryPanel.getLDIF(userEntry(), NEW_DN, "uid", "", SCHEMA);
+
+    assertThat(valuesOf(readEntry(ldif), "uid")).containsExactly("user.2");
+  }
+
+  /**
+   * The attributes used in an RDN have a case insensitive equality matching rule, so the value
+   * stored in the entry may differ from the value written in the DN. The old value must be
+   * replaced nonetheless, otherwise the duplicated entry keeps the value of the original RDN.
+   */
+  @Test
+  public void testMultiValuedRDNAttributeReplacesTheOldValueCaseInsensitively() throws Exception
+  {
+    Entry entry = new LinkedHashMapEntry(
+        "dn: cn=John Smith,ou=people,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "cn: john smith",
+        "cn: Johnny",
+        "sn: Smith");
+
+    String ldif = DuplicateEntryPanel.getLDIF(entry, "cn=John Smith-1,ou=people,dc=example,dc=com", "cn", "", SCHEMA);
+
+    assertThat(valuesOf(readEntry(ldif), "cn")).containsExactlyInAnyOrder("John Smith-1", "Johnny");
+  }
+
+  /** Attributes the user cannot modify must not be copied into the duplicated entry. */
+  @Test
+  public void testNoUserModificationAttributesAreNotCopied() throws Exception
+  {
+    Entry entry = new LinkedHashMapEntry(
+        "dn: uid=user.1,ou=people,dc=example,dc=com",
+        "objectClass: top",
+        "objectClass: person",
+        "uid: user.1",
+        "createTimestamp: 20260101000000Z",
+        "sn: One");
+
+    String ldif = DuplicateEntryPanel.getLDIF(entry, NEW_DN, "uid", "", SCHEMA);
+
+    Entry duplicate = readEntry(ldif);
+    assertThat(valuesOf(duplicate, "createTimestamp")).isEmpty();
+    assertThat(valuesOf(duplicate, "sn")).containsExactly("One");
+  }
+
+  /** Attributes with a binary syntax must be base64 encoded. */
+  @Test
+  public void testBinaryAttributesAreBase64Encoded() throws Exception
+  {
+    Entry entry = new LinkedHashMapEntry(DN.valueOf("uid=user.1,ou=people,dc=example,dc=com"))
+        .addAttribute("objectClass", "top", "person")
+        .addAttribute("uid", "user.1")
+        .addAttribute("sn", "One")
+        .addAttribute("userCertificate", ByteString.wrap(new byte[] { 0x30, 0x00, (byte) 0xFF }));
+
+    String ldif = DuplicateEntryPanel.getLDIF(entry, NEW_DN, "uid", "", SCHEMA);
+
+    // "MAD/" is the base64 encoding of the three bytes above.
+    assertThat(ldif).contains("userCertificate:: MAD/");
+    assertThat(valuesOf(readEntry(ldif), "sn")).containsExactly("One");
+  }
+
+  /**
+   * Reads the generated LDIF the same way {@code AbstractNewEntryPanel.getEntry()} does: an empty
+   * line terminates the record, so anything written after one is not part of the entry that is
+   * added to the server.
+   */
+  private static Entry readEntry(String ldif) throws IOException
+  {
+    try (LDIFEntryReader reader = new LDIFEntryReader(new StringReader(ldif)))
+    {
+      return reader.readEntry();
+    }
+  }
+
+  private static List<String> valuesOf(Entry entry, String attrName)
+  {
+    List<String> values = new ArrayList<>();
+    Attribute attr = entry.getAttribute(attrName);
+    if (attr != null)
+    {
+      for (ByteString value : attr)
+      {
+        values.add(value.toString());
+      }
+    }
+    return values;
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanelTestCase.java b/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanelTestCase.java
new file mode 100644
index 0000000..75ccfb5
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/SimplifiedViewEntryPanelTestCase.java
@@ -0,0 +1,72 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.guitools.controlpanel.ui;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.forgerock.opendj.ldap.ByteString;
+import org.opends.server.DirectoryServerTestCase;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/** Tests the object class counting done by {@link SimplifiedViewEntryPanel}. */
+@SuppressWarnings("javadoc")
+public class SimplifiedViewEntryPanelTestCase extends DirectoryServerTestCase
+{
+  @DataProvider
+  public Object[][] objectClassValues()
+  {
+    return new Object[][] {
+      { Collections.<String> emptyList(), 0 },
+      { list("top"), 0 },
+      // Object class values are case insensitive: "top" must be filtered out whatever its case is.
+      { list("TOP"), 0 },
+      { list("Top"), 0 },
+      { list("top", "person"), 1 },
+      { list("TOP", "person"), 1 },
+      { list("top", "person", "organizationalPerson"), 2 },
+      { list("person", "organizationalPerson"), 2 },
+    };
+  }
+
+  @Test(dataProvider = "objectClassValues")
+  public void testCountObjectClassesBesidesTop(List<String> objectClasses, int expectedCount)
+  {
+    assertThat(SimplifiedViewEntryPanel.countObjectClassesBesidesTop(byteStrings(objectClasses)))
+        .isEqualTo(expectedCount);
+  }
+
+  private static List<String> list(String... values)
+  {
+    List<String> result = new ArrayList<>();
+    Collections.addAll(result, values);
+    return result;
+  }
+
+  private static List<ByteString> byteStrings(List<String> values)
+  {
+    List<ByteString> result = new ArrayList<>();
+    for (String value : values)
+    {
+      result.add(ByteString.valueOfUtf8(value));
+    }
+    return result;
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanelTestCase.java b/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanelTestCase.java
new file mode 100644
index 0000000..932f9cb
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/guitools/controlpanel/ui/TableViewEntryPanelTestCase.java
@@ -0,0 +1,69 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.guitools.controlpanel.ui;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.opends.server.util.CollectionUtils.newHashSet;
+
+import java.util.Set;
+
+import org.forgerock.opendj.ldap.AttributeDescription;
+import org.forgerock.opendj.ldap.schema.Schema;
+import org.opends.server.DirectoryServerTestCase;
+import org.testng.annotations.Test;
+
+/** Tests how {@link TableViewEntryPanel} decides what to display for a given attribute. */
+@SuppressWarnings("javadoc")
+public class TableViewEntryPanelTestCase extends DirectoryServerTestCase
+{
+  private static final Schema SCHEMA = Schema.getCoreSchema();
+  private static final AttributeDescription OBJECT_CLASS = AttributeDescription.valueOf("objectClass", SCHEMA);
+
+  @Test
+  public void testObjectClassIsDisplayedAsADescriptorWhenTheSchemaIsAvailable()
+  {
+    assertThat(TableViewEntryPanel.isObjectClassWithSchema(OBJECT_CLASS, SCHEMA)).isTrue();
+  }
+
+  /**
+   * The object class descriptor cannot be built without the schema: the raw values must then be
+   * displayed instead, otherwise the object class attribute disappears from the table.
+   */
+  @Test
+  public void testObjectClassFallsBackToItsRawValuesWithoutSchema()
+  {
+    assertThat(TableViewEntryPanel.isObjectClassWithSchema(OBJECT_CLASS, null)).isFalse();
+  }
+
+  @Test
+  public void testOtherAttributesAreNeverDisplayedAsADescriptor()
+  {
+    assertThat(TableViewEntryPanel.isObjectClassWithSchema(AttributeDescription.valueOf("cn", SCHEMA), SCHEMA))
+        .isFalse();
+  }
+
+  /** Attribute names are case insensitive, and the required attributes are stored in lower case. */
+  @Test
+  public void testRequiredAttributesAreMatchedCaseInsensitively()
+  {
+    Set<String> requiredAttrs = newHashSet("objectclass", "sn", "cn");
+
+    assertThat(TableViewEntryPanel.isRequired(requiredAttrs, OBJECT_CLASS)).isTrue();
+    assertThat(TableViewEntryPanel.isRequired(requiredAttrs, AttributeDescription.valueOf("sn", SCHEMA))).isTrue();
+    assertThat(TableViewEntryPanel.isRequired(requiredAttrs, AttributeDescription.valueOf("description", SCHEMA)))
+        .isFalse();
+  }
+}
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/util/cli/LDAPConnectionConsoleInteractionTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/util/cli/LDAPConnectionConsoleInteractionTestCase.java
new file mode 100644
index 0000000..5a4fef5
--- /dev/null
+++ b/opendj-server-legacy/src/test/java/org/opends/server/util/cli/LDAPConnectionConsoleInteractionTestCase.java
@@ -0,0 +1,59 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.opends.server.util.cli;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.opends.messages.ToolMessages.INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI;
+import static org.opends.messages.ToolMessages.INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI;
+
+import org.opends.admin.ads.util.ApplicationTrustManager;
+import org.opends.server.DirectoryServerTestCase;
+import org.testng.annotations.Test;
+
+/** Tests the message shown when the certificate presented by a server is rejected. */
+@SuppressWarnings("javadoc")
+public class LDAPConnectionConsoleInteractionTestCase extends DirectoryServerTestCase
+{
+  private static final String HOST = "localhost";
+  private static final int PORT = 4444;
+
+  @Test
+  public void testUntrustedCertificateIsReported()
+  {
+    assertThat(rejectionMessage(ApplicationTrustManager.Cause.NOT_TRUSTED))
+        .isEqualTo(INFO_CERTIFICATE_NOT_TRUSTED_TEXT_CLI.get(HOST, PORT).toString());
+  }
+
+  @Test
+  public void testHostNameMismatchIsReported()
+  {
+    assertThat(rejectionMessage(ApplicationTrustManager.Cause.HOST_NAME_MISMATCH))
+        .isEqualTo(INFO_CERTIFICATE_NAME_MISMATCH_TEXT_CLI.get(HOST, PORT, HOST, HOST, PORT).toString());
+  }
+
+  /** The two causes must not lead to the same message: the user needs to know what is wrong. */
+  @Test
+  public void testTheTwoCausesAreReportedDifferently()
+  {
+    assertThat(rejectionMessage(ApplicationTrustManager.Cause.NOT_TRUSTED))
+        .isNotEqualTo(rejectionMessage(ApplicationTrustManager.Cause.HOST_NAME_MISMATCH));
+  }
+
+  private static String rejectionMessage(ApplicationTrustManager.Cause cause)
+  {
+    return LDAPConnectionConsoleInteraction.getCertificateRejectionMessage(cause, HOST, PORT).toString();
+  }
+}

--
Gitblit v1.10.0