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..a0c9794878e 100644
--- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java
+++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java
@@ -263,6 +263,23 @@ public List getExposedPorts() {
return exposedPorts;
}
+ /**
+ * {@inheritDoc}
+ *
+ * Unlike the default implementation, this preserves the protocol the first port was exposed with,
+ * so it also works for containers whose first exposed port is UDP.
+ */
+ @Override
+ public Integer getFirstMappedPort() {
+ return this.containerDef.getExposedPorts()
+ .stream()
+ .findFirst()
+ .map(exposedPort ->
+ getMappedPort(exposedPort.getPort(), InternetProtocol.fromDockerNotation(exposedPort.getProtocol().toString()))
+ )
+ .orElseThrow(() -> new IllegalStateException("Container doesn't expose any ports"));
+ }
+
@Override
public void setExposedPorts(List exposedPorts) {
this.containerDef.exposedPorts.clear();
@@ -1048,6 +1065,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");
+ }
}