diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index e19f7a85310..459261a91c5 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 a specific 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,17 @@ 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.name()) + ); + 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/main/java/org/testcontainers/containers/GenericContainer.java b/core/src/main/java/org/testcontainers/containers/GenericContainer.java index 4d3778c63d1..1006d8ca5c6 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -1048,6 +1048,30 @@ public void addExposedPorts(int... ports) { this.containerDef.addExposedTcpPorts(ports); } + /** + * Expose a container port using a specific protocol, so that a randomly chosen host port will be bound to it + * when the container starts. Use {@link ContainerState#getMappedPort(int, InternetProtocol)} to retrieve the + * bound host port once the container is running. + * + * @param port the container port to expose + * @param protocol the protocol (TCP or UDP) that the port should be exposed with + */ + public void addExposedPort(int port, InternetProtocol protocol) { + this.containerDef.addExposedPort(port, com.github.dockerjava.api.model.InternetProtocol.parse(protocol.name())); + } + + /** + * Fluent variant of {@link #addExposedPort(int, InternetProtocol)}. + * + * @param port the container port to expose + * @param protocol the protocol (TCP or UDP) that the port should be exposed with + * @return this + */ + public SELF withExposedPort(int port, InternetProtocol protocol) { + addExposedPort(port, protocol); + return self(); + } + /** * {@inheritDoc} */ diff --git a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java index 7b37bc1926f..515c5f17ae8 100644 --- a/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java +++ b/core/src/test/java/org/testcontainers/containers/ContainerStateTest.java @@ -1,12 +1,20 @@ 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 org.mockito.Mockito; import java.util.Collections; import java.util.List; import static org.assertj.core.api.Assertions.assertThat; +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 +43,37 @@ void test(String name, String testSet, List expectedResult) { List result = containerState.getBoundPortNumbers(); assertThat(result).hasSameElementsAs(expectedResult); } + + @Test + void getMappedPortWithProtocolLooksUpTheBindingForThatProtocol() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(anyInt(), any()); + when(containerState.getContainerId()).thenReturn("container-id"); + + InspectContainerResponse containerInfo = Mockito.mock(InspectContainerResponse.class, Answers.RETURNS_DEEP_STUBS); + ExposedPort udpPort = new ExposedPort(12345, com.github.dockerjava.api.model.InternetProtocol.UDP); + when(containerInfo.getNetworkSettings().getPorts().getBindings()) + .thenReturn(Collections.singletonMap(udpPort, new Ports.Binding[] { Ports.Binding.bindPort(54321) })); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + Integer mappedPort = containerState.getMappedPort(12345, InternetProtocol.UDP); + + assertThat(mappedPort).isEqualTo(54321); + } + + @Test + void getMappedPortWithProtocolThrowsWhenNotMapped() { + ContainerState containerState = mock(ContainerState.class); + doCallRealMethod().when(containerState).getMappedPort(anyInt(), any()); + when(containerState.getContainerId()).thenReturn("container-id"); + + InspectContainerResponse containerInfo = Mockito.mock(InspectContainerResponse.class, Answers.RETURNS_DEEP_STUBS); + when(containerInfo.getNetworkSettings().getPorts().getBindings()).thenReturn(Collections.emptyMap()); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + + org.assertj.core.api.Assertions + .assertThatThrownBy(() -> containerState.getMappedPort(12345, InternetProtocol.UDP)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("12345/udp"); + } }