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: