1package docker
2
3import (
4 "context"
5 "net/http"
6 "net/http/httptest"
7 "strings"
8 "testing"
9
10 "github.com/moby/moby/client"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/gordon/internal/domain"
15)
16
17// backendBindsRuntime builds a docker runtime whose container inspection
18// reports the given port mappings.
19func backendBindsRuntime(t *testing.T, portsJSON string) *Runtime {
20 t.Helper()
21 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
22 if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/containers/abc123/json") {
23 w.Header().Set("Content-Type", "application/json")
24 _, _ = w.Write([]byte(`{"Id":"abc123","NetworkSettings":{"Ports":` + portsJSON + `}}`))
25 return
26 }
27 t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
28 }))
29 t.Cleanup(server.Close)
30
31 host := strings.TrimPrefix(server.URL, "http://")
32 cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client()))
33 require.NoError(t, err)
34 return NewRuntimeWithClient(cli)
35}
36
37// TestRuntime_GetContainerBackendBindsRejectsMalformedMappings proves only
38// an exact single loopback binding is accepted: a wildcard, non-loopback,
39// empty, or duplicated mapping fails closed.
40func TestRuntime_GetContainerBackendBindsRejectsMalformedMappings(t *testing.T) {
41 cases := []struct {
42 name string
43 ports string
44 }{
45 {"wildcard", `{"9000/tcp":[{"HostIp":"0.0.0.0","HostPort":"32771"}]}`},
46 {"non-loopback", `{"9000/tcp":[{"HostIp":"192.168.1.5","HostPort":"32771"}]}`},
47 {"empty host ip", `{"9000/tcp":[{"HostIp":"","HostPort":"32771"}]}`},
48 {"multiple bindings", `{"9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"32771"},{"HostIp":"127.0.0.1","HostPort":"32772"}]}`},
49 {"invalid host port", `{"9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"0"}]}`},
50 }
51 for _, tc := range cases {
52 t.Run(tc.name, func(t *testing.T) {
53 runtime := backendBindsRuntime(t, tc.ports)
54 _, err := runtime.GetContainerBackendBinds(context.Background(), "abc123", []domain.ContainerBackendPort{
55 {ContainerPort: 9000, Protocol: domain.NetworkProtocolTCP},
56 })
57 require.Error(t, err)
58 })
59 }
60}
61
62// TestRuntime_GetContainerBackendBindsResolvesTCPAndUDPTogether proves one
63// inspection resolves both a TCP and a UDP bind, including the same
64// container port number on both protocols (distinct sockets).
65func TestRuntime_GetContainerBackendBindsResolvesTCPAndUDPTogether(t *testing.T) {
66 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
67 if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/containers/abc123/json") {
68 w.Header().Set("Content-Type", "application/json")
69 _, _ = w.Write([]byte(`{
70 "Id":"abc123",
71 "NetworkSettings":{"Ports":{
72 "5432/tcp":[{"HostIp":"127.0.0.1","HostPort":"32770"}],
73 "9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"32771"}],
74 "9000/udp":[{"HostIp":"127.0.0.1","HostPort":"32772"}]
75 }}
76 }`))
77 return
78 }
79 t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
80 }))
81 defer server.Close()
82
83 host := strings.TrimPrefix(server.URL, "http://")
84 cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client()))
85 require.NoError(t, err)
86
87 runtime := NewRuntimeWithClient(cli)
88 binds, err := runtime.GetContainerBackendBinds(context.Background(), "abc123", []domain.ContainerBackendPort{
89 {ContainerPort: 5432, Protocol: domain.NetworkProtocolTCP},
90 {ContainerPort: 9000, Protocol: domain.NetworkProtocolTCP},
91 {ContainerPort: 9000, Protocol: domain.NetworkProtocolUDP},
92 })
93 require.NoError(t, err)
94 require.Len(t, binds, 3)
95 assert.Equal(t, domain.ContainerBackendBind{ContainerPort: 5432, HostPort: 32770, Protocol: domain.NetworkProtocolTCP}, binds[0])
96 assert.Equal(t, domain.ContainerBackendBind{ContainerPort: 9000, HostPort: 32771, Protocol: domain.NetworkProtocolTCP}, binds[1])
97 assert.Equal(t, domain.ContainerBackendBind{ContainerPort: 9000, HostPort: 32772, Protocol: domain.NetworkProtocolUDP}, binds[2])
98}
99
100// TestRuntime_GetContainerBackendBindsFailsWhenProtocolMissing proves a
101// missing protocol-specific mapping fails closed instead of falling
102// back to the other protocol's bind.
103func TestRuntime_GetContainerBackendBindsFailsWhenProtocolMissing(t *testing.T) {
104 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
105 if r.Method == http.MethodGet && strings.HasSuffix(r.URL.Path, "/containers/abc123/json") {
106 w.Header().Set("Content-Type", "application/json")
107 _, _ = w.Write([]byte(`{
108 "Id":"abc123",
109 "NetworkSettings":{"Ports":{
110 "9000/tcp":[{"HostIp":"127.0.0.1","HostPort":"32771"}]
111 }}
112 }`))
113 return
114 }
115 t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
116 }))
117 defer server.Close()
118
119 host := strings.TrimPrefix(server.URL, "http://")
120 cli, err := client.New(client.WithHost("tcp://"+host), client.WithAPIVersion("1.41"), client.WithHTTPClient(server.Client()))
121 require.NoError(t, err)
122
123 runtime := NewRuntimeWithClient(cli)
124 _, err = runtime.GetContainerBackendBinds(context.Background(), "abc123", []domain.ContainerBackendPort{
125 {ContainerPort: 9000, Protocol: domain.NetworkProtocolUDP},
126 })
127 require.ErrorContains(t, err, "9000/udp")
128}