-
-
Notifications
You must be signed in to change notification settings - Fork 603
Expand file tree
/
Copy pathudp_port_binding_test.go
More file actions
175 lines (146 loc) · 6.09 KB
/
udp_port_binding_test.go
File metadata and controls
175 lines (146 loc) · 6.09 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
package testcontainers
import (
"context"
"net"
"net/netip"
"testing"
"time"
"github.com/moby/moby/api/types/network"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestUDPPortBinding tests the fix for the UDP port binding issue.
// This addresses the bug where exposed UDP ports always returned "0" instead of the actual mapped port.
//
// Background: When using ExposedPorts: []string{"8080/udp"}, the MappedPort() function
// would return "0/udp" instead of the actual host port like "55051/udp".
//
// Root cause: nat.ParsePortSpecs() creates PortBinding with empty HostPort (""),
// but Docker needs HostPort: "0" for automatic port allocation.
//
// Fix: In mergePortBindings(), convert empty HostPort to "0" for auto-allocation.
func TestUDPPortBinding(t *testing.T) {
ctx := context.Background()
t.Run("UDP port gets proper host port allocation", func(t *testing.T) {
// Create container with UDP port exposed
req := ContainerRequest{
Image: "alpine/socat:latest",
ExposedPorts: []string{"8080/udp"},
Cmd: []string{"UDP-LISTEN:8080,fork,reuseaddr", "EXEC:'/bin/cat'"},
}
container, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
defer func() {
assert.NoError(t, container.Terminate(ctx))
}()
// Test MappedPort function - this was the bug
udpPort := "8080/udp"
mappedPort, err := container.MappedPort(ctx, udpPort)
require.NoError(t, err)
// Before fix: mappedPort.Port() would return "0"
// After fix: mappedPort.Port() returns actual port like "55051"
assert.NotEqual(t, "0", mappedPort.Port(), "UDP port should not return '0'")
assert.Equal(t, network.UDP, mappedPort.Proto(), "Protocol should be UDP")
portNum := mappedPort.Num()
assert.Positive(t, portNum, "Port number should be greater than 0")
// Verify the port is actually accessible (basic connectivity test)
hostIP, err := container.Host(ctx)
require.NoError(t, err)
address := net.JoinHostPort(hostIP, mappedPort.Port())
conn, err := net.DialTimeout("udp", address, 2*time.Second)
require.NoError(t, err, "Should be able to connect to UDP port")
conn.Close()
})
t.Run("TCP port continues to work (regression test)", func(t *testing.T) {
// Ensure our UDP fix doesn't break TCP ports
req := ContainerRequest{
Image: "nginx:alpine",
ExposedPorts: []string{"80/tcp"},
}
container, err := GenericContainer(ctx, GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
defer func() {
assert.NoError(t, container.Terminate(ctx))
}()
tcpPort := "80/tcp"
mappedPort, err := container.MappedPort(ctx, tcpPort)
require.NoError(t, err)
assert.NotEqual(t, "0", mappedPort.Port(), "TCP port should not return '0'")
assert.Equal(t, network.TCP, mappedPort.Proto(), "Protocol should be TCP")
portNum := mappedPort.Num()
assert.Positive(t, portNum, "Port number should be greater than 0")
})
}
// TestPortBindingInternalLogic tests the internal mergePortBindings function
// that was modified to fix the UDP port binding issue.
func TestPortBindingInternalLogic(t *testing.T) {
t.Run("mergePortBindings fixes empty HostPort", func(t *testing.T) {
// Test the core fix: empty HostPort should become "0"
// This simulates what nat.ParsePortSpecs returns for "8080/udp"
port := network.MustParsePort("8080/udp")
exposedPortSet := network.PortSet{
port: struct{}{},
}
configPortMap := network.PortMap{
port: []network.PortBinding{{HostPort: ""}}, // Empty HostPort (the bug)
}
// Call the function our fix modified
result := mergePortBindings(configPortMap, exposedPortSet)
// Verify the fix worked
require.Contains(t, result, port)
bindings := result[port]
require.Len(t, bindings, 1)
// THE KEY ASSERTION: Empty HostPort should become "0"
assert.Equal(t, "0", bindings[0].HostPort,
"Empty HostPort should be converted to '0' for auto-allocation")
assert.Zero(t, bindings[0].HostIP, "HostIP should remain empty for all interfaces")
})
t.Run("mergePortBindings preserves existing HostPort", func(t *testing.T) {
// Ensure we don't modify already-set HostPort values
port := network.MustParsePort("8080/udp")
exposedPortSet := network.PortSet{
port: struct{}{},
}
configPortMap := network.PortMap{
port: []network.PortBinding{{HostIP: netip.MustParseAddr("127.0.0.1"), HostPort: "9090"}},
}
result := mergePortBindings(configPortMap, exposedPortSet)
bindings := result[port]
require.Len(t, bindings, 1)
// Should preserve existing values
assert.Equal(t, "9090", bindings[0].HostPort, "Existing HostPort should be preserved")
assert.Equal(t, "127.0.0.1", bindings[0].HostIP.String(), "Existing HostIP should be preserved")
})
t.Run("nat.ParsePortSpecs behavior documentation", func(t *testing.T) {
// This test documents the behavior of nat.ParsePortSpecs that caused the bug
exposedPorts := []string{"8080/udp", "9090/tcp"}
exposedPortSet := network.PortSet{
network.MustParsePort(exposedPorts[0]): struct{}{},
network.MustParsePort(exposedPorts[1]): struct{}{},
}
configPortMap := network.PortMap{
network.MustParsePort(exposedPorts[0]): []network.PortBinding{{HostPort: ""}},
network.MustParsePort(exposedPorts[1]): []network.PortBinding{{HostPort: ""}},
}
// Call mergePortBindings which normalizes empty HostPort
result := mergePortBindings(configPortMap, exposedPortSet)
// Verify the port set
assert.Contains(t, exposedPortSet, network.MustParsePort("8080/udp"))
assert.Contains(t, exposedPortSet, network.MustParsePort("9090/tcp"))
// Document the problematic behavior: nat.ParsePortSpecs creates empty HostPort
udpBindings := result[network.MustParsePort("8080/udp")]
require.Len(t, udpBindings, 1)
assert.Equal(t, "0", udpBindings[0].HostPort,
"Empty HostPort should be converted to '0' for auto-allocation")
tcpBindings := result[network.MustParsePort("9090/tcp")]
require.Len(t, tcpBindings, 1)
assert.Equal(t, "0", tcpBindings[0].HostPort,
"Empty HostPort should be converted to '0' for auto-allocation")
})
}