1package container
2
3import (
4 "errors"
5 "testing"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9 "go.uber.org/mock/gomock"
10
11 "gordon/internal/config"
12 "gordon/internal/testutils"
13 "gordon/internal/testutils/mocks"
14 "gordon/pkg/runtime"
15)
16
17func TestManager_GetNetworkForApp_WithGroups(t *testing.T) {
18 manager := &Manager{
19 config: &config.Config{
20 NetworkIsolation: config.NetworkIsolationConfig{
21 Enabled: true,
22 NetworkPrefix: "gordon",
23 },
24 NetworkGroups: map[string][]string{
25 "backend": {"api.example.com", "worker.example.com"},
26 "frontend": {"web.example.com", "cdn.example.com"},
27 },
28 },
29 }
30
31 // Test app in backend group
32 network := manager.GetNetworkForApp("api.example.com")
33 assert.Equal(t, "gordon-backend", network)
34
35 // Test app in frontend group
36 network = manager.GetNetworkForApp("web.example.com")
37 assert.Equal(t, "gordon-frontend", network)
38
39 // Test standalone app
40 network = manager.GetNetworkForApp("standalone.example.com")
41 assert.Equal(t, "gordon-standalone-example-com", network)
42
43 // Test disabled network isolation
44 manager.config.NetworkIsolation.Enabled = false
45 network = manager.GetNetworkForApp("test.example.com")
46 assert.Equal(t, "bridge", network)
47}
48
49func TestManager_generateNetworkName(t *testing.T) {
50 manager := &Manager{
51 config: &config.Config{
52 NetworkIsolation: config.NetworkIsolationConfig{
53 NetworkPrefix: "gordon",
54 },
55 },
56 }
57
58 networkName := manager.generateNetworkName("test.example.com")
59 assert.Equal(t, "gordon-test-example-com", networkName)
60
61 networkName = manager.generateNetworkName("backend")
62 assert.Equal(t, "gordon-backend", networkName)
63
64 networkName = manager.generateNetworkName("complex-app.subdomain.example.com")
65 assert.Equal(t, "gordon-complex-app-subdomain-example-com", networkName)
66}
67
68func TestManager_GetAppsForNetwork(t *testing.T) {
69 manager := &Manager{
70 config: &config.Config{
71 NetworkGroups: map[string][]string{
72 "backend": {"api.example.com", "worker.example.com"},
73 "frontend": {"web.example.com"},
74 },
75 },
76 }
77
78 // Test network group
79 apps := manager.GetAppsForNetwork("backend")
80 assert.Equal(t, []string{"api.example.com", "worker.example.com"}, apps)
81
82 // Test single app group
83 apps = manager.GetAppsForNetwork("frontend")
84 assert.Equal(t, []string{"web.example.com"}, apps)
85
86 // Test single app (not in any group)
87 apps = manager.GetAppsForNetwork("standalone.example.com")
88 assert.Equal(t, []string{"standalone.example.com"}, apps)
89}
90
91func TestManager_CreateNetworkIfNeeded(t *testing.T) {
92 ctrl := gomock.NewController(t)
93 defer ctrl.Finish()
94
95 ctx := testutils.TestContext(t)
96 mockRuntime := mocks.NewMockRuntime(ctrl)
97
98 cfg := &config.Config{}
99 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
100 require.NoError(t, err)
101
102 // Test bridge network (should not try to create)
103 err = manager.CreateNetworkIfNeeded(ctx, "bridge")
104 assert.NoError(t, err)
105
106 // Test default network (should not try to create)
107 err = manager.CreateNetworkIfNeeded(ctx, "default")
108 assert.NoError(t, err)
109
110 // Test custom network that doesn't exist
111 mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test").Return(false, nil)
112 mockRuntime.EXPECT().CreateNetwork(ctx, "gordon-test", map[string]string{"driver": "bridge"}).Return(nil)
113
114 err = manager.CreateNetworkIfNeeded(ctx, "gordon-test")
115 assert.NoError(t, err)
116
117 // Test custom network that already exists
118 mockRuntime.EXPECT().NetworkExists(ctx, "gordon-existing").Return(true, nil)
119
120 err = manager.CreateNetworkIfNeeded(ctx, "gordon-existing")
121 assert.NoError(t, err)
122}
123
124func TestManager_CreateNetworkIfNeeded_Errors(t *testing.T) {
125 ctrl := gomock.NewController(t)
126 defer ctrl.Finish()
127
128 ctx := testutils.TestContext(t)
129 mockRuntime := mocks.NewMockRuntime(ctrl)
130
131 cfg := &config.Config{}
132 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
133 require.NoError(t, err)
134
135 // Test error checking if network exists
136 mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test").Return(false, errors.New("check failed"))
137
138 err = manager.CreateNetworkIfNeeded(ctx, "gordon-test")
139 assert.Error(t, err)
140 assert.Contains(t, err.Error(), "failed to check if network exists")
141
142 // Test error creating network
143 mockRuntime.EXPECT().NetworkExists(ctx, "gordon-test").Return(false, nil)
144 mockRuntime.EXPECT().CreateNetwork(ctx, "gordon-test", map[string]string{"driver": "bridge"}).Return(errors.New("create failed"))
145
146 err = manager.CreateNetworkIfNeeded(ctx, "gordon-test")
147 assert.Error(t, err)
148 assert.Contains(t, err.Error(), "failed to create network")
149}
150
151func TestManager_cleanupNetworkIfEmpty(t *testing.T) {
152 ctrl := gomock.NewController(t)
153 defer ctrl.Finish()
154
155 ctx := testutils.TestContext(t)
156 mockRuntime := mocks.NewMockRuntime(ctrl)
157
158 cfg := &config.Config{}
159 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
160 require.NoError(t, err)
161
162 // Test cleanup of empty network
163 networks := []*runtime.NetworkInfo{
164 {
165 Name: "gordon-test",
166 Containers: []string{}, // Empty network
167 },
168 {
169 Name: "gordon-used",
170 Containers: []string{"container1"}, // Has containers
171 },
172 }
173
174 mockRuntime.EXPECT().ListNetworks(ctx).Return(networks, nil)
175 mockRuntime.EXPECT().RemoveNetwork(ctx, "gordon-test").Return(nil)
176
177 err = manager.cleanupNetworkIfEmpty(ctx, "gordon-test")
178 assert.NoError(t, err)
179
180 // Test network with containers (should not be removed)
181 mockRuntime.EXPECT().ListNetworks(ctx).Return(networks, nil)
182
183 err = manager.cleanupNetworkIfEmpty(ctx, "gordon-used")
184 assert.NoError(t, err)
185
186 // Test bridge network (should not try to remove)
187 err = manager.cleanupNetworkIfEmpty(ctx, "bridge")
188 assert.NoError(t, err)
189
190 // Test default network (should not try to remove)
191 err = manager.cleanupNetworkIfEmpty(ctx, "default")
192 assert.NoError(t, err)
193}
194
195func TestManager_cleanupNetworkIfEmpty_Errors(t *testing.T) {
196 ctrl := gomock.NewController(t)
197 defer ctrl.Finish()
198
199 ctx := testutils.TestContext(t)
200 mockRuntime := mocks.NewMockRuntime(ctrl)
201
202 cfg := &config.Config{}
203 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
204 require.NoError(t, err)
205
206 // Test error listing networks
207 mockRuntime.EXPECT().ListNetworks(ctx).Return(nil, errors.New("list failed"))
208
209 err = manager.cleanupNetworkIfEmpty(ctx, "gordon-test")
210 assert.Error(t, err)
211 assert.Contains(t, err.Error(), "failed to list networks")
212
213 // Test error removing network
214 networks := []*runtime.NetworkInfo{
215 {
216 Name: "gordon-test",
217 Containers: []string{},
218 },
219 }
220
221 mockRuntime.EXPECT().ListNetworks(ctx).Return(networks, nil)
222 mockRuntime.EXPECT().RemoveNetwork(ctx, "gordon-test").Return(errors.New("remove failed"))
223
224 err = manager.cleanupNetworkIfEmpty(ctx, "gordon-test")
225 assert.Error(t, err)
226}
227
228func TestManager_DeployAttachedService_SharedService(t *testing.T) {
229 ctrl := gomock.NewController(t)
230 defer ctrl.Finish()
231
232 ctx := testutils.TestContext(t)
233 mockRuntime := mocks.NewMockRuntime(ctrl)
234
235 cfg := &config.Config{
236 Volumes: config.VolumeConfig{
237 AutoCreate: false,
238 },
239 NetworkIsolation: config.NetworkIsolationConfig{
240 NetworkPrefix: "gordon",
241 },
242 NetworkGroups: map[string][]string{
243 "backend": {"api.example.com", "worker.example.com"},
244 },
245 }
246
247 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
248 require.NoError(t, err)
249
250 // Test deploying shared service for network group
251 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
252 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil)
253 mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{"POSTGRES_VERSION=15"}, nil)
254
255 expectedContainer := &runtime.Container{
256 ID: "postgres-service",
257 Name: "gordon-shared-postgres",
258 }
259
260 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
261 mockRuntime.EXPECT().StartContainer(ctx, "postgres-service").Return(nil)
262
263 err = manager.DeployAttachedService(ctx, "backend", "postgres:latest")
264
265 assert.NoError(t, err)
266}
267
268func TestManager_DeployAttachedService_AppSpecific(t *testing.T) {
269 ctrl := gomock.NewController(t)
270 defer ctrl.Finish()
271
272 ctx := testutils.TestContext(t)
273 mockRuntime := mocks.NewMockRuntime(ctrl)
274
275 cfg := &config.Config{
276 Volumes: config.VolumeConfig{
277 AutoCreate: false,
278 },
279 NetworkIsolation: config.NetworkIsolationConfig{
280 NetworkPrefix: "gordon",
281 },
282 }
283
284 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
285 require.NoError(t, err)
286
287 // Test deploying app-specific service
288 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
289 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil)
290 mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil)
291
292 expectedContainer := &runtime.Container{
293 ID: "redis-service",
294 Name: "api-example-com-redis",
295 }
296
297 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
298 mockRuntime.EXPECT().StartContainer(ctx, "redis-service").Return(nil)
299
300 err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest")
301
302 assert.NoError(t, err)
303}
304
305func TestManager_DeployAttachedService_AlreadyRunning(t *testing.T) {
306 ctrl := gomock.NewController(t)
307 defer ctrl.Finish()
308
309 ctx := testutils.TestContext(t)
310 mockRuntime := mocks.NewMockRuntime(ctrl)
311
312 cfg := &config.Config{
313 NetworkIsolation: config.NetworkIsolationConfig{
314 NetworkPrefix: "gordon",
315 },
316 }
317
318 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
319 require.NoError(t, err)
320
321 // Mock existing container with same name
322 existingContainers := []*runtime.Container{
323 {
324 ID: "existing-redis",
325 Name: "api-example-com-redis",
326 },
327 }
328
329 mockRuntime.EXPECT().ListContainers(ctx, false).Return(existingContainers, nil)
330
331 err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest")
332
333 assert.NoError(t, err)
334}
335
336func TestManager_DeployAttachedService_WithVolumes(t *testing.T) {
337 ctrl := gomock.NewController(t)
338 defer ctrl.Finish()
339
340 ctx := testutils.TestContext(t)
341 mockRuntime := mocks.NewMockRuntime(ctrl)
342
343 cfg := &config.Config{
344 Volumes: config.VolumeConfig{
345 AutoCreate: true,
346 Prefix: "gordon",
347 },
348 NetworkIsolation: config.NetworkIsolationConfig{
349 NetworkPrefix: "gordon",
350 },
351 }
352
353 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
354 require.NoError(t, err)
355
356 // Test service with volumes
357 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
358 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "postgres:latest").Return([]int{5432}, nil)
359 mockRuntime.EXPECT().InspectImageEnv(ctx, "postgres:latest").Return([]string{}, nil)
360 mockRuntime.EXPECT().InspectImageVolumes(ctx, "postgres:latest").Return([]string{"/var/lib/postgresql/data"}, nil)
361
362 // Volume creation expectations
363 mockRuntime.EXPECT().VolumeExists(ctx, "gordon-api-example-com-var-lib-postgresql-data").Return(false, nil)
364 mockRuntime.EXPECT().CreateVolume(ctx, "gordon-api-example-com-var-lib-postgresql-data").Return(nil)
365
366 expectedContainer := &runtime.Container{
367 ID: "postgres-service",
368 Name: "api-example-com-postgres",
369 }
370
371 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(expectedContainer, nil)
372 mockRuntime.EXPECT().StartContainer(ctx, "postgres-service").Return(nil)
373
374 err = manager.DeployAttachedService(ctx, "api.example.com", "postgres:latest")
375
376 assert.NoError(t, err)
377}
378
379func TestManager_DeployAttachedService_Errors(t *testing.T) {
380 ctrl := gomock.NewController(t)
381 defer ctrl.Finish()
382
383 ctx := testutils.TestContext(t)
384 mockRuntime := mocks.NewMockRuntime(ctrl)
385
386 cfg := &config.Config{
387 Volumes: config.VolumeConfig{
388 AutoCreate: false,
389 },
390 NetworkIsolation: config.NetworkIsolationConfig{
391 NetworkPrefix: "gordon",
392 },
393 }
394
395 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
396 require.NoError(t, err)
397
398 // Test create container error
399 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
400 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil)
401 mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil)
402 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(nil, errors.New("create failed"))
403
404 err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest")
405 assert.Error(t, err)
406 assert.Contains(t, err.Error(), "failed to create service container")
407
408 // Test start container error
409 mockRuntime.EXPECT().ListContainers(ctx, false).Return([]*runtime.Container{}, nil)
410 mockRuntime.EXPECT().GetImageExposedPorts(ctx, "redis:latest").Return([]int{6379}, nil)
411 mockRuntime.EXPECT().InspectImageEnv(ctx, "redis:latest").Return([]string{}, nil)
412
413 container := &runtime.Container{ID: "service123", Name: "api-example-com-redis"}
414 mockRuntime.EXPECT().CreateContainer(ctx, gomock.Any()).Return(container, nil)
415 mockRuntime.EXPECT().StartContainer(ctx, "service123").Return(errors.New("start failed"))
416 mockRuntime.EXPECT().RemoveContainer(ctx, "service123", true).Return(nil)
417
418 err = manager.DeployAttachedService(ctx, "api.example.com", "redis:latest")
419 assert.Error(t, err)
420 assert.Contains(t, err.Error(), "failed to start service container")
421}