From e3a05437fd362a1f1b53f9c0b7986a102c9ab033 Mon Sep 17 00:00:00 2001 From: glasses-and-hat Date: Sun, 5 Jul 2026 21:26:42 -0500 Subject: [PATCH] feat: add getMappedPort(int, InternetProtocol) overload Allows retrieving the host-mapped port for a container port that is exposed over UDP (or explicitly over TCP) instead of always assuming TCP, so containers that expose the same port number for both protocols (e.g. Jaeger's UDP ports) can be queried unambiguously. Fixes #554 --- .../containers/ContainerState.java | 28 ++++++++++- .../containers/ContainerStateTest.java | 46 +++++++++++++++++++ docs/features/networking.md | 3 ++ 3 files changed, 75 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index e19f7a85310..5ec1fe595e8 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -158,6 +158,24 @@ default Integer getFirstMappedPort() { * @see #getCurrentContainerInfo() */ default Integer getMappedPort(int originalPort) { + return getMappedPort(originalPort, InternetProtocol.TCP); + } + + /** + * Get the actual mapped port for a given port exposed by the container, for the given protocol. + * 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()}. + * + * @param originalPort the original port that is exposed + * @param protocol the protocol (TCP or UDP) that the port is exposed with + * @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, InternetProtocol protocol) { Preconditions.checkState( this.getContainerId() != null, "Mapped port can only be obtained after the container is started" @@ -166,13 +184,19 @@ default Integer getMappedPort(int originalPort) { Ports.Binding[] binding = new Ports.Binding[0]; final InspectContainerResponse containerInfo = this.getContainerInfo(); if (containerInfo != null) { - binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort)); + ExposedPort exposedPort = new ExposedPort( + originalPort, + com.github.dockerjava.api.model.InternetProtocol.parse(protocol.toDockerNotation()) + ); + binding = containerInfo.getNetworkSettings().getPorts().getBindings().get(exposedPort); } if (binding != null && binding.length > 0 && binding[0] != null) { return Integer.valueOf(binding[0].getHostPortSpec()); } else { - throw new IllegalArgumentException("Requested port (" + originalPort + ") is not mapped"); + throw new IllegalArgumentException( + "Requested port (" + originalPort + "/" + protocol.toDockerNotation() + ") is not mapped" + ); } } diff --git a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java index 7b37bc1926f..ede6c462c48 100644 --- a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java +++ b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java @@ -1,12 +1,22 @@ package org.testcontainers.containers; +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.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.Answers; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -35,4 +45,40 @@ void test(String name, String testSet, List expectedResult) { List result = containerState.getBoundPortNumbers(); assertThat(result).hasSameElementsAs(expectedResult); } + + @Test + void getMappedPortDistinguishesTcpAndUdpBindingsForSamePort() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(anyInt()); + doCallRealMethod().when(containerState).getMappedPort(anyInt(), any()); + when(containerState.getContainerId()).thenReturn("container-id"); + + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class, Answers.RETURNS_DEEP_STUBS); + Map bindings = new HashMap<>(); + bindings.put(ExposedPort.tcp(8080), new Ports.Binding[] { Ports.Binding.bindPort(12345) }); + bindings.put(ExposedPort.udp(8080), new Ports.Binding[] { Ports.Binding.bindPort(54321) }); + when(containerInfo.getNetworkSettings().getPorts().getBindings()).thenReturn(bindings); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + assertThat(containerState.getMappedPort(8080)).isEqualTo(12345); + assertThat(containerState.getMappedPort(8080, InternetProtocol.TCP)).isEqualTo(12345); + assertThat(containerState.getMappedPort(8080, InternetProtocol.UDP)).isEqualTo(54321); + } + + @Test + void getMappedPortThrowsWhenPortNotMappedForProtocol() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(anyInt(), any()); + when(containerState.getContainerId()).thenReturn("container-id"); + + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class, Answers.RETURNS_DEEP_STUBS); + Map bindings = new HashMap<>(); + bindings.put(ExposedPort.tcp(8080), new Ports.Binding[] { Ports.Binding.bindPort(12345) }); + when(containerInfo.getNetworkSettings().getPorts().getBindings()).thenReturn(bindings); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + assertThatThrownBy(() -> containerState.getMappedPort(8080, InternetProtocol.UDP)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("udp"); + } } diff --git a/docs/features/networking.md b/docs/features/networking.md index 7ecf929da16..d1732b35f84 100644 --- a/docs/features/networking.md +++ b/docs/features/networking.md @@ -27,6 +27,9 @@ This can be done using the `getMappedPort` method, which takes the original (con Because the randomised port mapping happens during container startup, the container must be running at the time `getMappedPort` is called. You may need to ensure that the startup order of components in your tests caters for this. +`getMappedPort` assumes a TCP port by default. If a container exposes the same port number for both TCP and UDP, +pass the desired `InternetProtocol` explicitly, e.g. `getMappedPort(originalPort, InternetProtocol.UDP)`. + There is also a `getFirstMappedPort` method for convenience, for the fairly common scenario of a container that only exposes one port: