diff --git a/core/src/main/java/org/testcontainers/containers/Container.java b/core/src/main/java/org/testcontainers/containers/Container.java index abea7fef576..5d0ce15cfd0 100644 --- a/core/src/main/java/org/testcontainers/containers/Container.java +++ b/core/src/main/java/org/testcontainers/containers/Container.java @@ -123,6 +123,14 @@ default void addFileSystemBind(final String hostPath, final String containerPath */ void addExposedPorts(int... ports); + /** + * Add an exposed port with the specified protocol. + * + * @param port the port to expose + * @param protocol the protocol (TCP or UDP) + */ + void addExposedPort(int port, InternetProtocol protocol); + /** * Specify the {@link WaitStrategy} to use to determine if the container is ready. * diff --git a/core/src/main/java/org/testcontainers/containers/ContainerState.java b/core/src/main/java/org/testcontainers/containers/ContainerState.java index e19f7a85310..beb30a3f99f 100644 --- a/core/src/main/java/org/testcontainers/containers/ContainerState.java +++ b/core/src/main/java/org/testcontainers/containers/ContainerState.java @@ -145,7 +145,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 @@ -156,8 +156,29 @@ default Integer getFirstMappedPort() { * @return the port that the exposed port is mapped to, or null if it is not exposed * @see #getContainerInfo() * @see #getCurrentContainerInfo() + * @see #getMappedPort(int, InternetProtocol) */ 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 (TCP or UDP) of the exposed port + * @return the port that the exposed port is mapped to + * @throws IllegalStateException if the container is not started + * @throws IllegalArgumentException if the requested port 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 +187,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.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 0fe944433ae..d5c1e86cbe7 100644 --- a/core/src/main/java/org/testcontainers/containers/GenericContainer.java +++ b/core/src/main/java/org/testcontainers/containers/GenericContainer.java @@ -1048,6 +1048,14 @@ public void addExposedPorts(int... ports) { this.containerDef.addExposedTcpPorts(ports); } + @Override + public void addExposedPort(int port, InternetProtocol protocol) { + this.containerDef.addExposedPort( + port, + com.github.dockerjava.api.model.InternetProtocol.parse(protocol.name()) + ); + } + /** * {@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..8a74dfcec35 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.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; 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.Mockito.doCallRealMethod; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -35,4 +43,112 @@ void test(String name, String testSet, List expectedResult) { List result = containerState.getBoundPortNumbers(); assertThat(result).hasSameElementsAs(expectedResult); } + + @Test + void shouldGetMappedPortForUdpProtocol() { + ContainerState containerState = mock(ContainerState.class); + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + NetworkSettings networkSettings = mock(NetworkSettings.class); + Ports ports = mock(Ports.class); + + // Set up the mock port bindings for UDP port 5353 + Map bindings = new HashMap<>(); + ExposedPort udpPort = new ExposedPort(5353, com.github.dockerjava.api.model.InternetProtocol.UDP); + bindings.put(udpPort, new Ports.Binding[] { Ports.Binding.bindPort(12345) }); + + when(containerState.getContainerId()).thenReturn("test-container-id"); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(networkSettings.getPorts()).thenReturn(ports); + when(ports.getBindings()).thenReturn(bindings); + doCallRealMethod().when(containerState).getMappedPort(5353, InternetProtocol.UDP); + + Integer mappedPort = containerState.getMappedPort(5353, InternetProtocol.UDP); + assertThat(mappedPort).isEqualTo(12345); + } + + @Test + void shouldGetMappedPortForTcpUsingProtocol() { + ContainerState containerState = mock(ContainerState.class); + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + NetworkSettings networkSettings = mock(NetworkSettings.class); + Ports ports = mock(Ports.class); + + // Set up the mock port bindings for TCP port 8080 + Map bindings = new HashMap<>(); + ExposedPort tcpPort = new ExposedPort(8080, com.github.dockerjava.api.model.InternetProtocol.TCP); + bindings.put(tcpPort, new Ports.Binding[] { Ports.Binding.bindPort(54321) }); + + when(containerState.getContainerId()).thenReturn("test-container-id"); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(networkSettings.getPorts()).thenReturn(ports); + when(ports.getBindings()).thenReturn(bindings); + doCallRealMethod().when(containerState).getMappedPort(8080, InternetProtocol.TCP); + doCallRealMethod().when(containerState).getMappedPort(8080); + + // Test with explicit TCP protocol + Integer mappedPort = containerState.getMappedPort(8080, InternetProtocol.TCP); + assertThat(mappedPort).isEqualTo(54321); + + // Test default getMappedPort (should also return same value since it defaults to TCP) + Integer defaultMappedPort = containerState.getMappedPort(8080); + assertThat(defaultMappedPort).isEqualTo(54321); + } + + @Test + void shouldThrowForUnmappedUdpPort() { + ContainerState containerState = mock(ContainerState.class); + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + NetworkSettings networkSettings = mock(NetworkSettings.class); + Ports ports = mock(Ports.class); + + // Set up the mock port bindings with only TCP port + Map bindings = new HashMap<>(); + ExposedPort tcpPort = new ExposedPort(8080, com.github.dockerjava.api.model.InternetProtocol.TCP); + bindings.put(tcpPort, new Ports.Binding[] { Ports.Binding.bindPort(54321) }); + + when(containerState.getContainerId()).thenReturn("test-container-id"); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(networkSettings.getPorts()).thenReturn(ports); + when(ports.getBindings()).thenReturn(bindings); + doCallRealMethod().when(containerState).getMappedPort(8080, InternetProtocol.UDP); + + // Should throw when trying to get unmapped UDP port + assertThatThrownBy(() -> containerState.getMappedPort(8080, InternetProtocol.UDP)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("8080/udp") + .hasMessageContaining("is not mapped"); + } + + @Test + void shouldSupportBothTcpAndUdpOnSamePort() { + ContainerState containerState = mock(ContainerState.class); + InspectContainerResponse containerInfo = mock(InspectContainerResponse.class); + NetworkSettings networkSettings = mock(NetworkSettings.class); + Ports ports = mock(Ports.class); + + // Set up the mock port bindings with both TCP and UDP on the same port + Map bindings = new HashMap<>(); + ExposedPort tcpPort = new ExposedPort(5000, com.github.dockerjava.api.model.InternetProtocol.TCP); + ExposedPort udpPort = new ExposedPort(5000, com.github.dockerjava.api.model.InternetProtocol.UDP); + bindings.put(tcpPort, new Ports.Binding[] { Ports.Binding.bindPort(11111) }); + bindings.put(udpPort, new Ports.Binding[] { Ports.Binding.bindPort(22222) }); + + when(containerState.getContainerId()).thenReturn("test-container-id"); + when(containerState.getContainerInfo()).thenReturn(containerInfo); + when(containerInfo.getNetworkSettings()).thenReturn(networkSettings); + when(networkSettings.getPorts()).thenReturn(ports); + when(ports.getBindings()).thenReturn(bindings); + doCallRealMethod().when(containerState).getMappedPort(5000, InternetProtocol.TCP); + doCallRealMethod().when(containerState).getMappedPort(5000, InternetProtocol.UDP); + + // Both should be mapped to different host ports + Integer tcpMappedPort = containerState.getMappedPort(5000, InternetProtocol.TCP); + Integer udpMappedPort = containerState.getMappedPort(5000, InternetProtocol.UDP); + + assertThat(tcpMappedPort).isEqualTo(11111); + assertThat(udpMappedPort).isEqualTo(22222); + } }