diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java
index e19f7a85310..a0327ebb221 100644
--- a/core/src/main/java/org/testcontainers/containers/ContainerState.java
+++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java
@@ -148,13 +148,12 @@ default Integer getFirstMappedPort() {
* Get the actual mapped port for a given port exposed by the container.
* It should be used in conjunction with {@link #getHost()}.
*
- * Note: The returned port number might be outdated (for instance, after disconnecting from a network and reconnecting
- * again). If you always need up-to-date value, override the {@link #getContainerInfo()} to return the
- * {@link #getCurrentContainerInfo()}.
+ * The host port is resolved from a live container inspect so it stays correct after Docker
+ * reassigns published ports (for example after disconnecting from a network and reconnecting,
+ * or after attaching the container to an additional network).
*
* @param originalPort the original TCP port that is exposed
* @return the port that the exposed port is mapped to, or null if it is not exposed
- * @see #getContainerInfo()
* @see #getCurrentContainerInfo()
*/
default Integer getMappedPort(int originalPort) {
@@ -164,7 +163,9 @@ default Integer getMappedPort(int originalPort) {
);
Ports.Binding[] binding = new Ports.Binding[0];
- final InspectContainerResponse containerInfo = this.getContainerInfo();
+ // Live inspect: cached {@link #getContainerInfo()} can hold stale host ports after
+ // network attach/detach (see https://github.com/testcontainers/testcontainers-java/issues/11779).
+ final InspectContainerResponse containerInfo = this.getCurrentContainerInfo();
if (containerInfo != null) {
binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort));
}
diff --git a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java
index 7b37bc1926f..2b33e4d5699 100644
--- a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java
+++ b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java
@@ -1,5 +1,10 @@
package org.testcontainers.containers;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.NetworkSettings;
+import com.github.dockerjava.api.model.Ports;
+import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
@@ -35,4 +40,48 @@ void test(String name, String testSet, List expectedResult) {
List result = containerState.getBoundPortNumbers();
assertThat(result).hasSameElementsAs(expectedResult);
}
+
+ @Test
+ void getMappedPortUsesCurrentContainerInfo() {
+ ContainerState containerState = mock(ContainerState.class);
+ doCallRealMethod().when(containerState).getMappedPort(8080);
+ InspectContainerResponse cachedInfo = inspectResponse(8080, 18080);
+ InspectContainerResponse currentInfo = inspectResponse(8080, 28080);
+
+ when(containerState.getContainerId()).thenReturn("container-id");
+ when(containerState.getContainerInfo()).thenReturn(cachedInfo);
+ when(containerState.getCurrentContainerInfo()).thenReturn(currentInfo);
+
+ assertThat(containerState.getMappedPort(8080)).isEqualTo(28080);
+ }
+
+ @Test
+ void getFirstMappedPortUsesCurrentContainerInfo() {
+ ContainerState containerState = mock(ContainerState.class);
+ doCallRealMethod().when(containerState).getMappedPort(8080);
+ doCallRealMethod().when(containerState).getFirstMappedPort();
+ InspectContainerResponse cachedInfo = inspectResponse(8080, 18080);
+ InspectContainerResponse currentInfo = inspectResponse(8080, 28080);
+
+ when(containerState.getContainerId()).thenReturn("container-id");
+ when(containerState.getExposedPorts()).thenReturn(Collections.singletonList(8080));
+ when(containerState.getContainerInfo()).thenReturn(cachedInfo);
+ when(containerState.getCurrentContainerInfo()).thenReturn(currentInfo);
+
+ assertThat(containerState.getFirstMappedPort()).isEqualTo(28080);
+ }
+
+ private static InspectContainerResponse inspectResponse(int containerPort, int hostPort) {
+ InspectContainerResponse response = mock(InspectContainerResponse.class);
+ NetworkSettings networkSettings = mock(NetworkSettings.class);
+ Ports ports = new Ports();
+ ExposedPort exposedPort = new ExposedPort(containerPort);
+ Ports.Binding binding = Ports.Binding.bindPort(hostPort);
+ ports.bind(exposedPort, binding);
+
+ when(response.getNetworkSettings()).thenReturn(networkSettings);
+ when(networkSettings.getPorts()).thenReturn(ports);
+
+ return response;
+ }
}
diff --git a/core/src/test/java/org/testcontainers/containers/NetworkTest.java b/core/src/test/java/org/testcontainers/containers/NetworkTest.java
index 675658b353a..538498db8c1 100644
--- a/core/src/test/java/org/testcontainers/containers/NetworkTest.java
+++ b/core/src/test/java/org/testcontainers/containers/NetworkTest.java
@@ -1,5 +1,9 @@
package org.testcontainers.containers;
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.InspectContainerResponse;
+import com.github.dockerjava.api.model.ExposedPort;
+import com.github.dockerjava.api.model.Ports;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.testcontainers.DockerClientFactory;
@@ -109,5 +113,115 @@ void testReusability() {
.isNotEqualTo(firstId);
}
}
+
+ @Test
+ void getFirstMappedPortRemainsCorrectAfterSecondaryNetworkAttach() {
+ // Regression for https://github.com/testcontainers/testcontainers-java/issues/11779
+ try (
+ Network primary = Network.newNetwork();
+ Network secondary = Network.newNetwork();
+ GenericContainer> container = new GenericContainer<>(TestImages.TINY_IMAGE)
+ .withNetwork(primary)
+ .withNetworkAliases("test-host")
+ .withExposedPorts(7077)
+ .withCommand(
+ "/bin/sh",
+ "-c",
+ "while true ; do printf 'HTTP/1.1 200 OK\\n\\nok' | nc -l -p 7077; done"
+ )
+ ) {
+ container.start();
+
+ Integer mappedPortBefore = container.getFirstMappedPort();
+ assertThat(mappedPortBefore).isEqualTo(publishedHostPort(container, 7077));
+
+ DockerClient dockerClient = DockerClientFactory.instance().client();
+ dockerClient
+ .connectToNetworkCmd()
+ .withContainerId(container.getContainerId())
+ .withNetworkId(secondary.getId())
+ .exec();
+
+ assertThat(container.getFirstMappedPort())
+ .as("mapped port after secondary network attach matches Docker")
+ .isEqualTo(publishedHostPort(container, 7077));
+ assertThat(container.getMappedPort(7077)).isEqualTo(publishedHostPort(container, 7077));
+ }
+ }
+
+ @Test
+ void getMappedPortReflectsDockerRemapAfterNetworkDisconnectReconnect() {
+ // Docker reassigns published host ports after network disconnect/reconnect.
+ // Cached inspect data must not be used for mapped-port lookups.
+ try (
+ Network primary = Network.newNetwork();
+ Network secondary = Network.newNetwork();
+ GenericContainer> container = new GenericContainer<>(TestImages.TINY_IMAGE)
+ .withNetwork(primary)
+ .withExposedPorts(7077)
+ .withCommand(
+ "/bin/sh",
+ "-c",
+ "while true ; do printf 'HTTP/1.1 200 OK\\n\\nok' | nc -l -p 7077; done"
+ )
+ ) {
+ container.start();
+
+ Integer mappedPortBefore = container.getMappedPort(7077);
+ Integer cachedPortBefore = hostPortFromInspect(container.getContainerInfo(), 7077);
+ assertThat(mappedPortBefore).isEqualTo(cachedPortBefore);
+
+ DockerClient dockerClient = DockerClientFactory.instance().client();
+ dockerClient
+ .connectToNetworkCmd()
+ .withContainerId(container.getContainerId())
+ .withNetworkId(secondary.getId())
+ .exec();
+ dockerClient
+ .disconnectFromNetworkCmd()
+ .withContainerId(container.getContainerId())
+ .withNetworkId(primary.getId())
+ .exec();
+ dockerClient
+ .connectToNetworkCmd()
+ .withContainerId(container.getContainerId())
+ .withNetworkId(primary.getId())
+ .exec();
+
+ Integer publishedAfter = publishedHostPort(container, 7077);
+ Integer stillCachedFromStart = hostPortFromInspect(container.getContainerInfo(), 7077);
+
+ // When Docker remaps the host port, the start-time cache is stale — getMappedPort
+ // must still return Docker's current binding (not the cached one).
+ if (!publishedAfter.equals(stillCachedFromStart)) {
+ assertThat(container.getMappedPort(7077))
+ .as("must not return stale cached host port after network remap")
+ .isNotEqualTo(stillCachedFromStart)
+ .isEqualTo(publishedAfter);
+ } else {
+ assertThat(container.getMappedPort(7077)).isEqualTo(publishedAfter);
+ }
+ assertThat(container.getFirstMappedPort()).isEqualTo(publishedAfter);
+ }
+ }
+
+ private static Integer publishedHostPort(GenericContainer> container, int containerPort) {
+ InspectContainerResponse inspect = DockerClientFactory
+ .instance()
+ .client()
+ .inspectContainerCmd(container.getContainerId())
+ .exec();
+ return hostPortFromInspect(inspect, containerPort);
+ }
+
+ private static Integer hostPortFromInspect(InspectContainerResponse inspect, int containerPort) {
+ Ports.Binding[] bindings = inspect
+ .getNetworkSettings()
+ .getPorts()
+ .getBindings()
+ .get(new ExposedPort(containerPort));
+ assertThat(bindings).isNotNull().isNotEmpty();
+ return Integer.valueOf(bindings[0].getHostPortSpec());
+ }
}
}