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