From d954c4c53f3d626f2a60c86eed653569dfd907fe Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Mon, 19 Oct 2026 14:48:13 +0000
Subject: [PATCH] [#1215] Check a replication server the topology already has against the configured ones again, from the URL it is known with or the one it now comes with (#1218)

---
 opendj-server-legacy/src/test/java/org/opends/server/replication/service/ReplicationBrokerTest.java |  350 ++++++++++++++++++++++++++++++++++++++
 opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java     |  131 +++++++++++++
 2 files changed, 473 insertions(+), 8 deletions(-)

diff --git a/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java b/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java
index af26f3e..ae97bd3 100644
--- a/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java
+++ b/opendj-server-legacy/src/main/java/org/opends/server/replication/service/ReplicationBroker.java
@@ -3126,6 +3126,8 @@
         {
           // Update the existing info for the replication server
           rsInfo.update(newRSInfo, connectedDSs);
+          updateLocallyConfiguredFlag(rsInfo, newRSInfo.getServerUrl(),
+              configuredReplicationServerUrls, dsServerId);
         }
       }
 
@@ -3184,19 +3186,132 @@
         rsInfo.setLocallyConfigured(false);
         return;
       }
-      for (String serverUrl : configuredReplicationServerUrls)
+      final String serverUrl =
+          findEquivalentConfiguredUrl(rsUrl, configuredReplicationServerUrls);
+      if (serverUrl != null)
       {
-        if (isSameReplicationServerUrl(serverUrl, rsUrl))
-        {
-          // This RS is locally configured, mark this
-          rsInfo.setLocallyConfigured(true);
-          rsInfo.setServerURL(serverUrl);
-          return;
-        }
+        // This RS is locally configured, mark this
+        rsInfo.setLocallyConfigured(true);
+        rsInfo.setServerURL(serverUrl);
+        return;
       }
       rsInfo.setLocallyConfigured(false);
     }
 
+    /**
+     * Sets the locally configured flag for a ReplicationServerInfo object the
+     * previous topology already had: the configured replication servers may
+     * have changed since its flag was set.
+     * <p>
+     * The server is locally configured when the URL it is known with, or the
+     * one it now comes with, is a configured one. This needs no host name
+     * resolution, unlike setLocallyConfiguredFlag(): a server found
+     * locally configured is known with the configured URL, and a phase-one
+     * handshake names a server with the configured URL it reached it at. So a
+     * resolution failing for a moment cannot take the flag from a server known
+     * with a configured URL.
+     * <p>
+     * A server which has the flag, but whose URL the configured ones no longer
+     * have, is checked once more, resolving host names, with the URL it is known
+     * with and then with the one it comes with, which is the URL a new server is
+     * checked with: the configured URL may only have been written another way,
+     * and the restart of the session which a change of the configuration asks
+     * for is refused while a total update into this replica runs. A server found
+     * so is known with the configured URL from then on; any other loses the
+     * flag, keeps the URL it is known with, and is not resolved again. A
+     * resolution failing at that moment takes the flag.
+     *
+     * @param rsInfo
+     *          the Replication server to check and update
+     * @param newServerUrl
+     *          the URL the server comes with in the new topology
+     * @param configuredReplicationServerUrls
+     *          the configured replication server URLs
+     * @param dsServerId
+     *          the server id of the local DS, for the trace
+     */
+    private void updateLocallyConfiguredFlag(ReplicationServerInfo rsInfo,
+        String newServerUrl, Set<String> configuredReplicationServerUrls,
+        int dsServerId)
+    {
+      final boolean wasLocallyConfigured = rsInfo.isLocallyConfigured();
+      final String knownServerUrl = rsInfo.getServerURL();
+      if (isConfigured(knownServerUrl, configuredReplicationServerUrls))
+      {
+        rsInfo.setLocallyConfigured(true);
+      }
+      else if (isConfigured(newServerUrl, configuredReplicationServerUrls))
+      {
+        rsInfo.setLocallyConfigured(true);
+        rsInfo.setServerURL(newServerUrl);
+      }
+      else if (wasLocallyConfigured)
+      {
+        String configuredUrl = findEquivalentConfiguredUrl(knownServerUrl,
+            configuredReplicationServerUrls);
+        if (configuredUrl == null
+            && !Objects.equals(newServerUrl, knownServerUrl))
+        {
+          configuredUrl = findEquivalentConfiguredUrl(newServerUrl,
+              configuredReplicationServerUrls);
+        }
+        rsInfo.setLocallyConfigured(configuredUrl != null);
+        if (configuredUrl != null)
+        {
+          rsInfo.setServerURL(configuredUrl);
+        }
+      }
+      else
+      {
+        rsInfo.setLocallyConfigured(false);
+      }
+
+      // The previous topology holds the same object, so its comparison with the
+      // new one cannot show this change
+      if (logger.isTraceEnabled()
+          && (wasLocallyConfigured != rsInfo.isLocallyConfigured()
+              || !Objects.equals(knownServerUrl, rsInfo.getServerURL())))
+      {
+        logger.trace("RS(%d) on DS(%d): locally configured %s -> %s, URL %s -> %s",
+            rsInfo.getServerId(), dsServerId, wasLocallyConfigured,
+            rsInfo.isLocallyConfigured(), knownServerUrl, rsInfo.getServerURL());
+      }
+    }
+
+    private boolean isConfigured(String rsUrl,
+        Set<String> configuredReplicationServerUrls)
+    {
+      // A server with no URL (protocol version < 4) cannot be connected to
+      return rsUrl != null && configuredReplicationServerUrls.contains(rsUrl);
+    }
+
+    /**
+     * Returns the configured replication server URL which names the same server
+     * as the passed URL, resolving host names.
+     *
+     * @param rsUrl
+     *          the URL of the server, or null if it has none
+     * @param configuredReplicationServerUrls
+     *          the configured replication server URLs
+     * @return the configured URL which names the same server, or null if none
+     *         does or if the server has no URL
+     */
+    private String findEquivalentConfiguredUrl(String rsUrl,
+        Set<String> configuredReplicationServerUrls)
+    {
+      if (rsUrl != null)
+      {
+        for (String serverUrl : configuredReplicationServerUrls)
+        {
+          if (isSameReplicationServerUrl(serverUrl, rsUrl))
+          {
+            return serverUrl;
+          }
+        }
+      }
+      return null;
+    }
+
     @Override
     public boolean equals(Object obj)
     {
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/replication/service/ReplicationBrokerTest.java b/opendj-server-legacy/src/test/java/org/opends/server/replication/service/ReplicationBrokerTest.java
index 8ce97b7..d72b275 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/replication/service/ReplicationBrokerTest.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/replication/service/ReplicationBrokerTest.java
@@ -12,24 +12,31 @@
  * information: "Portions Copyright [year] [name of copyright owner]".
  *
  * Copyright 2014-2015 ForgeRock AS.
+ * Portions Copyright 2026 3A Systems, LLC.
  */
 package org.opends.server.replication.service;
 
+import java.net.InetAddress;
+import java.net.UnknownHostException;
 import java.util.*;
 
 import org.opends.server.DirectoryServerTestCase;
 import org.opends.server.replication.common.DSInfo;
 import org.opends.server.replication.common.RSInfo;
+import org.opends.server.replication.common.ServerState;
 import org.opends.server.replication.protocol.TopologyMsg;
+import org.opends.server.replication.service.ReplicationBroker.RSEvaluations;
 import org.opends.server.replication.service.ReplicationBroker.ReplicationServerInfo;
 import org.opends.server.replication.service.ReplicationBroker.Topology;
 import org.testng.Assert;
+import org.testng.SkipException;
 import org.testng.annotations.DataProvider;
 import org.testng.annotations.Test;
 
 import static java.util.Collections.*;
 
 import static org.assertj.core.api.Assertions.*;
+import static org.opends.server.replication.service.ReplicationBroker.computeBestReplicationServer;
 import static org.opends.server.util.CollectionUtils.*;
 import static org.testng.Assert.*;
 
@@ -254,4 +261,347 @@
     assertThat(currentRS.isLocallyConfigured()).isTrue();
     assertThat(anotherRS.isLocallyConfigured()).isFalse();
   }
+
+  /**
+   * A topology names a replication server before the configuration of the domain lists it: the next
+   * topology, built once the configuration lists it, must stamp it as locally configured.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldStampRSAddedToTheConfigurationAfterThePreviousTopology(TopologyCtorToUse toUse)
+  {
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "127.0.0.1:1389", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), newHashSet(CURRENT_RS.getServerURL()), EMPTY_MAP);
+    assertThat(find(before.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+
+    final RSInfo configuredRS = new RSInfo(ANOTHER_RS_ID, "localhost:1389", 0, (byte) 0, 0);
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), configuredRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "localhost:1389"), before.rsInfos);
+    assertInvariants(after);
+    final ReplicationServerInfo anotherRS = find(after.rsInfos, configuredRS);
+    assertThat(anotherRS.isLocallyConfigured()).isTrue();
+    assertThat(anotherRS.getServerURL()).isEqualTo("localhost:1389");
+    assertThat(find(after.rsInfos, CURRENT_RS.toRSInfo()).isLocallyConfigured()).isTrue();
+  }
+
+  /**
+   * A configured replication server announces in the topology messages a URL other than the
+   * configured one (a port mapped by NAT or by Docker), and the broker hears of it first that way.
+   * Once a phase-one handshake has reached it at the configured URL, it is locally configured, and
+   * stays so when the next topology message names it with the announced URL again.
+   */
+  @Test
+  @SuppressWarnings("unchecked")
+  public void topologyShouldStampRSReachedAtAConfiguredURLItDoesNotAnnounce()
+  {
+    final Set<String> configured = newHashSet(CURRENT_RS.getServerURL(), "localhost:28989");
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:8989", 0, (byte) 0, 0);
+    final Topology heard = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), configured, EMPTY_MAP);
+    assertThat(find(heard.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+
+    final RSInfo reachedRS = new RSInfo(ANOTHER_RS_ID, "localhost:28989", 0, (byte) 0, 0);
+    final Topology connected = newTopology(TopologyCtorToUse.BUILD_WITH_DS_RS_LISTS,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), reachedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID, configured, heard.rsInfos);
+    ReplicationServerInfo anotherRS = find(connected.rsInfos, reachedRS);
+    assertThat(anotherRS.isLocallyConfigured()).isTrue();
+    assertThat(anotherRS.getServerURL()).isEqualTo("localhost:28989");
+
+    final Topology next = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID, configured, connected.rsInfos);
+    anotherRS = find(next.rsInfos, announcedRS);
+    assertThat(anotherRS.isLocallyConfigured()).isTrue();
+    assertThat(anotherRS.getServerURL()).isEqualTo("localhost:28989");
+  }
+
+  /**
+   * A replication server the previous topology stamped as locally configured stays so as long as the
+   * configuration lists the URL it is known with, even when the topology names it with another URL (a port
+   * mapped by NAT or by Docker) and its host name cannot be resolved at that moment: losing the flag
+   * would make the broker leave the server it is connected to.
+   * <p>
+   * The case shows that the host is not resolved only where resolving it fails: a resolver which
+   * answers for any name (the fake addresses of a proxy or VPN DNS) skips it.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldKeepStampedAConfiguredRSWithoutResolvingItsHost(TopologyCtorToUse toUse)
+  {
+    try
+    {
+      final InetAddress[] answer = InetAddress.getAllByName("rs.invalid");
+      throw new SkipException("The resolver answers for rs.invalid with " + Arrays.toString(answer)
+          + ": this case could not tell a resolved host from one that is not");
+    }
+    catch (UnknownHostException expected)
+    {
+      // The premise of this case: resolving the host fails
+    }
+    final String unresolvableURL = "rs.invalid:1389";
+    final RSInfo connectedRS = new RSInfo(ANOTHER_RS_ID, unresolvableURL, 0, (byte) 0, 0);
+    final Map<Integer, ReplicationServerInfo> previousRSs =
+        newMap(new ReplicationServerInfo(connectedRS, newHashSet(CURRENT_DS.getDsId())));
+
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "rs.invalid:8989", 0, (byte) 0, 0);
+    final Topology topo = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(announcedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID, newHashSet(unresolvableURL), previousRSs);
+    assertInvariants(topo);
+    final ReplicationServerInfo anotherRS = find(topo.rsInfos, announcedRS);
+    assertThat(anotherRS.isLocallyConfigured()).isTrue();
+    assertThat(anotherRS.getServerURL()).isEqualTo(unresolvableURL);
+  }
+
+  /**
+   * A replication server is removed from the configuration of the domain: the next topology must no
+   * longer stamp it as locally configured.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldNotStampRSRemovedFromTheConfigurationAfterThePreviousTopology(TopologyCtorToUse toUse)
+  {
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:1389", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "localhost:1389"), EMPTY_MAP);
+    assertThat(find(before.rsInfos, announcedRS).isLocallyConfigured()).isTrue();
+
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), newHashSet(CURRENT_RS.getServerURL()), before.rsInfos);
+    assertInvariants(after);
+    assertThat(find(after.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+  }
+
+  /**
+   * A topology names a replication server before the configuration of the domain lists it, with the URL
+   * the configuration then lists it with: the next topology must stamp it as locally configured.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldStampRSAddedToTheConfigurationWithTheURLItAnnounces(TopologyCtorToUse toUse)
+  {
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:1389", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), newHashSet(CURRENT_RS.getServerURL()), EMPTY_MAP);
+    assertThat(find(before.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "localhost:1389"), before.rsInfos);
+    assertInvariants(after);
+    assertThat(find(after.rsInfos, announcedRS).isLocallyConfigured()).isTrue();
+  }
+
+  /**
+   * The broker has reconnected to a replication server a topology named before the configuration of
+   * the domain listed it, and the topology of the new session has the infos of the phase-one handshakes:
+   * the periodic check of the best replication server must not reject it as a server which is not
+   * locally configured.
+   * <p>
+   * The other server is not on this host, so the same-host filter keeps only this one, and the check
+   * returns it before it reads which server the broker is connected to: the case pins the filter of the
+   * locally configured servers only.
+   */
+  @Test
+  @SuppressWarnings("unchecked")
+  public void bestServerCheckShouldNotRejectTheRSAddedToTheConfiguration()
+  {
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:1389", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), newHashSet(CURRENT_RS.getServerURL()), EMPTY_MAP);
+
+    final Topology reconnected = newTopology(TopologyCtorToUse.BUILD_WITH_DS_RS_LISTS,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID,
+        newHashSet(CURRENT_RS.getServerURL(), "localhost:1389"), before.rsInfos);
+    final RSEvaluations evals = computeBestReplicationServer(false, ANOTHER_RS_ID, new ServerState(),
+        reconnected.rsInfos, CURRENT_DS.getDsId(), (byte) 0, 0);
+    assertThat(evals.getBestRS()).isNotNull();
+    assertThat(evals.getBestRS().getServerId()).as(evals.toString()).isEqualTo(ANOTHER_RS_ID);
+  }
+
+  /**
+   * The entry of the configuration which lists a replication server stamped as locally configured is
+   * written another way, with an equivalent URL: the server must keep the flag, with the configured URL,
+   * even when the restart of the session this asks for is refused (while a total update runs).
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldKeepStampedRSWhoseConfiguredURLIsWrittenAnotherWay(TopologyCtorToUse toUse)
+  {
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:8989", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "localhost:8989"), EMPTY_MAP);
+    assertThat(find(before.rsInfos, announcedRS).isLocallyConfigured()).isTrue();
+
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "127.0.0.1:8989"), before.rsInfos);
+    assertInvariants(after);
+    final ReplicationServerInfo anotherRS = find(after.rsInfos, announcedRS);
+    assertThat(anotherRS.isLocallyConfigured()).isTrue();
+    assertThat(anotherRS.getServerURL()).isEqualTo("127.0.0.1:8989");
+  }
+
+  /**
+   * A known replication server which is not locally configured comes with a URL which is only an
+   * equivalent of a configured one: the topology message must not resolve it, so the server gets the flag
+   * at the next connection only.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldNotResolveTheURLAKnownRSComesWith(TopologyCtorToUse toUse)
+  {
+    final Set<String> configured = newHashSet(CURRENT_RS.getServerURL(), "localhost:28989");
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:8989", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), configured, EMPTY_MAP);
+    assertThat(find(before.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+
+    final RSInfo equivalentRS = new RSInfo(ANOTHER_RS_ID, "127.0.0.1:28989", 0, (byte) 0, 0);
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), equivalentRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), configured, before.rsInfos);
+    assertInvariants(after);
+    assertThat(find(after.rsInfos, equivalentRS).isLocallyConfigured()).isFalse();
+  }
+
+  /**
+   * A known replication server which is not locally configured gets an equivalent of its URL in the
+   * configuration of the domain, written another way: the next topology must not resolve it either, so
+   * the server gets the flag at the next connection only.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldNotResolveTheURLAKnownRSIsKnownWith(TopologyCtorToUse toUse)
+  {
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "127.0.0.1:28989", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), newHashSet(CURRENT_RS.getServerURL()), EMPTY_MAP);
+    assertThat(find(before.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "localhost:28989"), before.rsInfos);
+    assertInvariants(after);
+    assertThat(find(after.rsInfos, announcedRS).isLocallyConfigured()).isFalse();
+  }
+
+  /**
+   * A known replication server comes with another URL in the next topology (a mapped port) while the
+   * configuration of the domain now lists the URL it is known with: it is locally configured, without
+   * resolving any host name.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldStampRSAddedToTheConfigurationWithTheURLItIsKnownWith(TopologyCtorToUse toUse)
+  {
+    final RSInfo knownRS = new RSInfo(ANOTHER_RS_ID, "127.0.0.2:28989", 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), knownRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), newHashSet(CURRENT_RS.getServerURL()), EMPTY_MAP);
+    assertThat(find(before.rsInfos, knownRS).isLocallyConfigured()).isFalse();
+
+    final RSInfo mappedRS = new RSInfo(ANOTHER_RS_ID, "127.0.0.3:28989", 0, (byte) 0, 0);
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), mappedRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(),
+        newHashSet(CURRENT_RS.getServerURL(), "127.0.0.2:28989"), before.rsInfos);
+    assertInvariants(after);
+    final ReplicationServerInfo rs = find(after.rsInfos, mappedRS);
+    assertThat(rs.isLocallyConfigured()).isTrue();
+    assertThat(rs.getServerURL()).isEqualTo("127.0.0.2:28989");
+  }
+
+  /**
+   * A known replication server with no URL (protocol version below 4) is not locally configured, and the
+   * check must not ask the configured URLs for a null one: they are a sorted set, as in the configuration,
+   * whose contains(null) throws.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldNotStampKnownRSWithoutURL(TopologyCtorToUse toUse)
+  {
+    // sorted, as the configuration is: contains(null) throws
+    final Set<String> configured = new TreeSet<>(Arrays.asList(CURRENT_RS.getServerURL(), "localhost:8989"));
+    final RSInfo noUrlRS = new RSInfo(ANOTHER_RS_ID, null, 0, (byte) 0, 0);
+    final Topology before = newTopology(TopologyCtorToUse.BUILD_WITH_TOPOLOGY_MSG,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), noUrlRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), configured, EMPTY_MAP);
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), noUrlRS),
+        CURRENT_DS.getDsId(), CURRENT_RS.getServerId(), configured, before.rsInfos);
+    assertThat(find(after.rsInfos, noUrlRS).isLocallyConfigured()).isFalse();
+  }
+
+  /**
+   * A locally configured replication server, known with the configured URL a phase-one handshake reached
+   * it at, comes with another URL (a port mapped by NAT or by Docker), and the entry of the configuration
+   * is written another way, as an equivalent of the URL it comes with: the server must keep the flag,
+   * with the configured URL.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldKeepStampedRSWhoseConfiguredURLIsWrittenAsTheURLItComesWith(TopologyCtorToUse toUse)
+  {
+    final RSInfo reachedRS = new RSInfo(ANOTHER_RS_ID, "localhost:28989", 0, (byte) 0, 0);
+    final Topology connected = newTopology(TopologyCtorToUse.BUILD_WITH_DS_RS_LISTS,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), reachedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID, newHashSet(CURRENT_RS.getServerURL(), "localhost:28989"), EMPTY_MAP);
+    assertThat(find(connected.rsInfos, reachedRS).isLocallyConfigured()).isTrue();
+
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:8989", 0, (byte) 0, 0);
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID,
+        newHashSet(CURRENT_RS.getServerURL(), "127.0.0.1:8989"), connected.rsInfos);
+    assertInvariants(after);
+    final ReplicationServerInfo rs = find(after.rsInfos, announcedRS);
+    assertThat(rs.isLocallyConfigured()).isTrue();
+    assertThat(rs.getServerURL()).isEqualTo("127.0.0.1:8989");
+  }
+
+  /**
+   * A locally configured replication server, known with the configured URL a phase-one handshake reached
+   * it at, comes with another URL, and the entry of the configuration is replaced with a URL equivalent to
+   * neither: the server loses the flag, and keeps the URL it is known with.
+   */
+  @Test(dataProvider = "topologyCtorProvider")
+  @SuppressWarnings("unchecked")
+  public void topologyShouldNotStampRSWhoseConfiguredURLNamesNeitherOfItsURLs(TopologyCtorToUse toUse)
+  {
+    final RSInfo reachedRS = new RSInfo(ANOTHER_RS_ID, "localhost:28989", 0, (byte) 0, 0);
+    final Topology connected = newTopology(TopologyCtorToUse.BUILD_WITH_DS_RS_LISTS,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), reachedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID, newHashSet(CURRENT_RS.getServerURL(), "localhost:28989"), EMPTY_MAP);
+    assertThat(find(connected.rsInfos, reachedRS).isLocallyConfigured()).isTrue();
+
+    final RSInfo announcedRS = new RSInfo(ANOTHER_RS_ID, "localhost:8989", 0, (byte) 0, 0);
+    final Topology after = newTopology(toUse,
+        newMap(OTHER_DS), newArrayList(CURRENT_RS.toRSInfo(), announcedRS),
+        CURRENT_DS.getDsId(), ANOTHER_RS_ID,
+        newHashSet(CURRENT_RS.getServerURL(), "127.0.0.1:18989"), connected.rsInfos);
+    assertInvariants(after);
+    final ReplicationServerInfo rs = find(after.rsInfos, announcedRS);
+    assertThat(rs.isLocallyConfigured()).isFalse();
+    assertThat(rs.getServerURL()).isEqualTo("localhost:28989");
+  }
 }

--
Gitblit v1.10.0