1package docker
2
3import (
4 "context"
5 "encoding/json"
6 "net/http"
7 "net/http/httptest"
8 "strings"
9 "testing"
10
11 "github.com/docker/docker/client"
12 "github.com/stretchr/testify/assert"
13 "github.com/stretchr/testify/require"
14
15 "github.com/bnema/gordon/internal/domain"
16)
17
18func TestRuntime_CreateContainerPublishesExplicitUDPPort(t *testing.T) {
19 createBody := createTestContainer(t, &domain.ContainerConfig{
20 Image: "rust:latest",
21 Name: "gordon-rust",
22 PortPublishes: []domain.ContainerPortPublish{
23 {
24 HostIP: "127.0.0.1",
25 HostPort: 38015,
26 ContainerPort: 28015,
27 Protocol: domain.NetworkProtocolUDP,
28 },
29 },
30 })
31
32 exposedPorts, ok := createBody["ExposedPorts"].(map[string]any)
33 require.True(t, ok)
34 assert.Contains(t, exposedPorts, "28015/udp")
35
36 hostConfig, ok := createBody["HostConfig"].(map[string]any)
37 require.True(t, ok)
38 portBindings, ok := hostConfig["PortBindings"].(map[string]any)
39 require.True(t, ok)
40 bindings, ok := portBindings["28015/udp"].([]any)
41 require.True(t, ok)
42 require.Len(t, bindings, 1)
43 binding, ok := bindings[0].(map[string]any)
44 require.True(t, ok)
45 assert.Equal(t, "127.0.0.1", binding["HostIp"])
46 assert.Equal(t, "38015", binding["HostPort"])
47}
48
49func TestRuntime_CreateContainerKeepsLegacyPortsRandomLoopbackTCP(t *testing.T) {
50 createBody := createTestContainer(t, &domain.ContainerConfig{
51 Image: "nginx:latest",
52 Name: "gordon-app.example.com",
53 Ports: []int{8080},
54 })
55
56 exposedPorts, ok := createBody["ExposedPorts"].(map[string]any)
57 require.True(t, ok)
58 assert.Contains(t, exposedPorts, "8080/tcp")
59
60 hostConfig, ok := createBody["HostConfig"].(map[string]any)
61 require.True(t, ok)
62 portBindings, ok := hostConfig["PortBindings"].(map[string]any)
63 require.True(t, ok)
64 bindings, ok := portBindings["8080/tcp"].([]any)
65 require.True(t, ok)
66 require.Len(t, bindings, 1)
67 binding, ok := bindings[0].(map[string]any)
68 require.True(t, ok)
69 assert.Equal(t, "127.0.0.1", binding["HostIp"])
70 assert.Equal(t, "0", binding["HostPort"])
71}
72
73func TestRuntime_CreateContainerAppliesRestartPolicy(t *testing.T) {
74 createBody := createTestContainer(t, &domain.ContainerConfig{
75 Image: "nginx:latest",
76 Name: "gordon-app.example.com",
77 RestartPolicy: domain.RestartPolicyAlways,
78 })
79
80 hostConfig, ok := createBody["HostConfig"].(map[string]any)
81 require.True(t, ok)
82 restartPolicy, ok := hostConfig["RestartPolicy"].(map[string]any)
83 require.True(t, ok)
84 assert.Equal(t, domain.RestartPolicyAlways, restartPolicy["Name"])
85}
86
87func createTestContainer(t *testing.T, config *domain.ContainerConfig) map[string]any {
88 t.Helper()
89
90 var createBody map[string]any
91 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
92 switch {
93 case r.Method == http.MethodPost && r.URL.Path == "/v1.41/containers/create":
94 require.NoError(t, json.NewDecoder(r.Body).Decode(&createBody))
95 w.Header().Set("Content-Type", "application/json")
96 _, _ = w.Write([]byte(`{"Id":"abc123","Warnings":null}`))
97 case r.Method == http.MethodGet && r.URL.Path == "/v1.41/containers/abc123/json":
98 w.Header().Set("Content-Type", "application/json")
99 _, _ = w.Write([]byte(`{
100 "Id":"abc123",
101 "Name":"/gordon-app.example.com",
102 "Image":"sha256:image",
103 "Created":"2026-05-05T00:00:00Z",
104 "Config":{"Image":"nginx:latest","Labels":{}},
105 "State":{"Status":"created","ExitCode":0},
106 "NetworkSettings":{"Ports":{}}
107 }`))
108 default:
109 t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
110 }
111 }))
112 defer server.Close()
113
114 host := strings.TrimPrefix(server.URL, "http://")
115 cli, err := client.NewClientWithOpts(client.WithHost("tcp://"+host), client.WithVersion("1.41"), client.WithHTTPClient(server.Client()))
116 require.NoError(t, err)
117
118 runtime := NewRuntimeWithClient(cli)
119 created, err := runtime.CreateContainer(context.Background(), config)
120 require.NoError(t, err)
121 assert.Equal(t, "abc123", created.ID)
122 require.NotNil(t, createBody)
123
124 return createBody
125}