From cb06f0ec5ac64f870cfb92d15dadb1ef04c66a71 Mon Sep 17 00:00:00 2001 From: Gorre Surya Date: Mon, 25 May 2026 22:47:52 -0400 Subject: [PATCH] feat: add getMappedPort(int, InternetProtocol) overload to ContainerState The existing getMappedPort(int) always looks up TCP bindings. UDP-exposed ports cannot be queried without manually traversing getContainerInfo(). Add an overload that accepts an InternetProtocol argument so callers can look up both TCP and UDP mapped ports through the same typed API. The no-arg overload is reimplemented to delegate to the new method with InternetProtocol.TCP so the two code paths share the same logic. Closes #554 Signed-off-by: Gorre Surya --- .../containers/ContainerState.java | 29 +++++++- .../containers/ContainerStateTest.java | 67 +++++++++++++++++++ 2 files changed, 93 insertions(+), 3 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..5b7940c4cdd 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -5,6 +5,7 @@ import com.github.dockerjava.api.command.InspectContainerResponse; import com.github.dockerjava.api.exception.DockerException; import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.InternetProtocol; import com.github.dockerjava.api.model.PortBinding; import com.github.dockerjava.api.model.Ports; import com.google.common.base.Preconditions; @@ -145,7 +146,7 @@ default Integer getFirstMappedPort() { } /** - * Get the actual mapped port for a given port exposed by the container. + * Get the actual mapped port for a given TCP 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 @@ -158,6 +159,25 @@ default Integer getFirstMappedPort() { * @see #getCurrentContainerInfo() */ default Integer getMappedPort(int originalPort) { + return getMappedPort(originalPort, InternetProtocol.TCP); + } + + /** + * Get the actual mapped port for a given port and protocol 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()}. + * + * @param originalPort the original port that is exposed + * @param protocol the protocol of the exposed port ({@link InternetProtocol#TCP} or {@link InternetProtocol#UDP}) + * @return the port that the exposed port is mapped to + * @throws IllegalArgumentException if the requested port/protocol combination is not mapped + * @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 +186,16 @@ 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)); + binding = + containerInfo.getNetworkSettings().getPorts().getBindings().get(new ExposedPort(originalPort, protocol)); } 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.toString().toLowerCase() + ") 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..db01a7390a0 100644 --- a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java +++ b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java @@ -1,5 +1,11 @@ package org.testcontainers.containers; +import com.github.dockerjava.api.command.InspectContainerResponse; +import com.github.dockerjava.api.model.ExposedPort; +import com.github.dockerjava.api.model.InternetProtocol; +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; @@ -7,6 +13,7 @@ import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -35,4 +42,64 @@ void test(String name, String testSet, List expectedResult) { List result = containerState.getBoundPortNumbers(); assertThat(result).hasSameElementsAs(expectedResult); } + + @Test + void getMappedPortWithTcpProtocolReturnsMappedPort() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(8080); + doCallRealMethod().when(containerState).getMappedPort(8080, InternetProtocol.TCP); + when(containerState.getContainerId()).thenReturn("test-container-id"); + + Ports ports = new Ports(); + ports.bind(new ExposedPort(8080, InternetProtocol.TCP), Ports.Binding.bindPort(32768)); + + NetworkSettings networkSettings = mock(NetworkSettings.class); + when(networkSettings.getPorts()).thenReturn(ports); + + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + assertThat(containerState.getMappedPort(8080)).isEqualTo(32768); + assertThat(containerState.getMappedPort(8080, InternetProtocol.TCP)).isEqualTo(32768); + } + + @Test + void getMappedPortWithUdpProtocolReturnsMappedPort() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(53, InternetProtocol.UDP); + when(containerState.getContainerId()).thenReturn("test-container-id"); + + Ports ports = new Ports(); + ports.bind(new ExposedPort(53, InternetProtocol.UDP), Ports.Binding.bindPort(32769)); + + NetworkSettings networkSettings = mock(NetworkSettings.class); + when(networkSettings.getPorts()).thenReturn(ports); + + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + assertThat(containerState.getMappedPort(53, InternetProtocol.UDP)).isEqualTo(32769); + } + + @Test + void getMappedPortThrowsWhenPortNotMapped() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(9999, InternetProtocol.UDP); + when(containerState.getContainerId()).thenReturn("test-container-id"); + + Ports ports = new Ports(); + + NetworkSettings networkSettings = mock(NetworkSettings.class); + when(networkSettings.getPorts()).thenReturn(ports); + + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + assertThatThrownBy(() -> containerState.getMappedPort(9999, InternetProtocol.UDP)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("9999/udp"); + } }