1package container
2
3import (
4 "errors"
5 "os"
6 "testing"
7
8 "github.com/stretchr/testify/assert"
9 "github.com/stretchr/testify/require"
10 "go.uber.org/mock/gomock"
11
12 "gordon/internal/config"
13 "gordon/internal/testutils"
14 "gordon/internal/testutils/mocks"
15 "gordon/pkg/runtime"
16)
17
18func TestManager_SyncContainers(t *testing.T) {
19 ctrl := gomock.NewController(t)
20 defer ctrl.Finish()
21
22 ctx := testutils.TestContext(t)
23 mockRuntime := mocks.NewMockRuntime(ctrl)
24
25 cfg := &config.Config{}
26 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
27 require.NoError(t, err)
28
29 // Add some containers to internal state that might be stale
30 manager.containers["old.example.com"] = &runtime.Container{ID: "old-container"}
31
32 // Mock runtime returning current containers
33 currentContainers := []*runtime.Container{
34 {
35 ID: "active-container-1",
36 Name: "gordon-app1.example.com",
37 Labels: map[string]string{
38 "gordon.managed": "true",
39 "gordon.domain": "app1.example.com",
40 },
41 },
42 {
43 ID: "active-container-2",
44 Name: "gordon-app2.example.com",
45 Labels: map[string]string{
46 "gordon.managed": "true",
47 "gordon.domain": "app2.example.com",
48 },
49 },
50 {
51 ID: "unmanaged-container",
52 Name: "some-other-container",
53 Labels: map[string]string{
54 "other.label": "value",
55 },
56 },
57 }
58
59 mockRuntime.EXPECT().ListContainers(ctx, false).Return(currentContainers, nil)
60
61 err = manager.SyncContainers(ctx)
62
63 assert.NoError(t, err)
64 assert.Len(t, manager.containers, 2)
65 assert.Contains(t, manager.containers, "app1.example.com")
66 assert.Contains(t, manager.containers, "app2.example.com")
67 assert.NotContains(t, manager.containers, "old.example.com")
68 assert.Equal(t, "active-container-1", manager.containers["app1.example.com"].ID)
69 assert.Equal(t, "active-container-2", manager.containers["app2.example.com"].ID)
70}
71
72func TestManager_SyncContainers_Error(t *testing.T) {
73 ctrl := gomock.NewController(t)
74 defer ctrl.Finish()
75
76 ctx := testutils.TestContext(t)
77 mockRuntime := mocks.NewMockRuntime(ctrl)
78
79 cfg := &config.Config{}
80 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
81 require.NoError(t, err)
82
83 mockRuntime.EXPECT().ListContainers(ctx, false).Return(nil, errors.New("runtime error"))
84
85 err = manager.SyncContainers(ctx)
86
87 assert.Error(t, err)
88 assert.Contains(t, err.Error(), "failed to list containers")
89}
90
91func TestManager_StopContainer(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 mockRuntime.EXPECT().StopContainer(ctx, "container123").Return(nil)
103
104 err = manager.StopContainer(ctx, "container123")
105
106 assert.NoError(t, err)
107}
108
109func TestManager_StopContainer_Error(t *testing.T) {
110 ctrl := gomock.NewController(t)
111 defer ctrl.Finish()
112
113 ctx := testutils.TestContext(t)
114 mockRuntime := mocks.NewMockRuntime(ctrl)
115
116 cfg := &config.Config{}
117 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
118 require.NoError(t, err)
119
120 mockRuntime.EXPECT().StopContainer(ctx, "container123").Return(errors.New("stop failed"))
121
122 err = manager.StopContainer(ctx, "container123")
123
124 assert.Error(t, err)
125 assert.Contains(t, err.Error(), "failed to stop container")
126}
127
128func TestManager_StopContainerByDomain(t *testing.T) {
129 ctrl := gomock.NewController(t)
130 defer ctrl.Finish()
131
132 ctx := testutils.TestContext(t)
133 mockRuntime := mocks.NewMockRuntime(ctrl)
134
135 cfg := &config.Config{}
136 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
137 require.NoError(t, err)
138
139 container := &runtime.Container{
140 ID: "container123",
141 }
142 manager.containers["test.example.com"] = container
143
144 mockRuntime.EXPECT().StopContainer(ctx, "container123").Return(nil)
145
146 err = manager.StopContainerByDomain(ctx, "test.example.com")
147
148 assert.NoError(t, err)
149}
150
151func TestManager_StopContainerByDomain_NotFound(t *testing.T) {
152 cfg := &config.Config{}
153 manager := &Manager{
154 config: cfg,
155 containers: make(map[string]*runtime.Container),
156 }
157
158 ctx := testutils.TestContext(t)
159 err := manager.StopContainerByDomain(ctx, "nonexistent.example.com")
160
161 assert.Error(t, err)
162 assert.Contains(t, err.Error(), "no container found for domain")
163}
164
165func TestManager_RemoveContainer(t *testing.T) {
166 ctrl := gomock.NewController(t)
167 defer ctrl.Finish()
168
169 ctx := testutils.TestContext(t)
170 mockRuntime := mocks.NewMockRuntime(ctrl)
171
172 cfg := &config.Config{
173 Volumes: config.VolumeConfig{
174 Preserve: true, // Don't cleanup volumes
175 },
176 NetworkIsolation: config.NetworkIsolationConfig{
177 Enabled: false,
178 },
179 }
180
181 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
182 require.NoError(t, err)
183
184 container := &runtime.Container{
185 ID: "container123",
186 }
187 manager.containers["test.example.com"] = container
188
189 mockRuntime.EXPECT().RemoveContainer(ctx, "container123", true).Return(nil)
190
191 err = manager.RemoveContainer(ctx, "container123", true)
192
193 assert.NoError(t, err)
194 assert.NotContains(t, manager.containers, "test.example.com")
195}
196
197func TestManager_RemoveContainer_WithVolumeCleanup(t *testing.T) {
198 ctrl := gomock.NewController(t)
199 defer ctrl.Finish()
200
201 ctx := testutils.TestContext(t)
202 mockRuntime := mocks.NewMockRuntime(ctrl)
203
204 cfg := &config.Config{
205 Volumes: config.VolumeConfig{
206 Preserve: false, // Should cleanup volumes
207 Prefix: "gordon",
208 },
209 NetworkIsolation: config.NetworkIsolationConfig{
210 Enabled: false,
211 },
212 Routes: map[string]string{
213 "test.example.com": "nginx:latest",
214 },
215 }
216
217 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
218 require.NoError(t, err)
219
220 container := &runtime.Container{
221 ID: "container123",
222 }
223 manager.containers["test.example.com"] = container
224
225 mockRuntime.EXPECT().RemoveContainer(ctx, "container123", true).Return(nil)
226
227 // Expect volume cleanup
228 mockRuntime.EXPECT().InspectImageVolumes(ctx, "nginx:latest").Return([]string{"/var/log/nginx"}, nil)
229 varLogNginxVolume := generateVolumeName("gordon", "test.example.com", "/var/log/nginx")
230 mockRuntime.EXPECT().VolumeExists(ctx, varLogNginxVolume).Return(true, nil)
231 mockRuntime.EXPECT().RemoveVolume(ctx, varLogNginxVolume, true).Return(nil)
232
233 err = manager.RemoveContainer(ctx, "container123", true)
234
235 assert.NoError(t, err)
236 assert.NotContains(t, manager.containers, "test.example.com")
237}
238
239func TestManager_RemoveContainer_WithNetworkCleanup(t *testing.T) {
240 ctrl := gomock.NewController(t)
241 defer ctrl.Finish()
242
243 ctx := testutils.TestContext(t)
244 mockRuntime := mocks.NewMockRuntime(ctrl)
245
246 cfg := &config.Config{
247 Volumes: config.VolumeConfig{
248 Preserve: true,
249 },
250 NetworkIsolation: config.NetworkIsolationConfig{
251 Enabled: true,
252 NetworkPrefix: "gordon",
253 },
254 }
255
256 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
257 require.NoError(t, err)
258
259 container := &runtime.Container{
260 ID: "container123",
261 }
262 manager.containers["test.example.com"] = container
263
264 mockRuntime.EXPECT().RemoveContainer(ctx, "container123", true).Return(nil)
265
266 // Expect network cleanup check
267 mockRuntime.EXPECT().ListNetworks(ctx).Return([]*runtime.NetworkInfo{
268 {
269 Name: "gordon-test-example-com",
270 Containers: []string{}, // Empty network
271 },
272 }, nil)
273 mockRuntime.EXPECT().RemoveNetwork(ctx, "gordon-test-example-com").Return(nil)
274
275 err = manager.RemoveContainer(ctx, "container123", true)
276
277 assert.NoError(t, err)
278 assert.NotContains(t, manager.containers, "test.example.com")
279}
280
281func TestManager_RemoveContainerByDomain(t *testing.T) {
282 ctrl := gomock.NewController(t)
283 defer ctrl.Finish()
284
285 ctx := testutils.TestContext(t)
286 mockRuntime := mocks.NewMockRuntime(ctrl)
287
288 cfg := &config.Config{
289 Volumes: config.VolumeConfig{
290 Preserve: true,
291 },
292 NetworkIsolation: config.NetworkIsolationConfig{
293 Enabled: false,
294 },
295 }
296
297 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
298 require.NoError(t, err)
299
300 container := &runtime.Container{
301 ID: "container123",
302 }
303 manager.containers["test.example.com"] = container
304
305 mockRuntime.EXPECT().RemoveContainer(ctx, "container123", false).Return(nil)
306
307 err = manager.RemoveContainerByDomain(ctx, "test.example.com", false)
308
309 assert.NoError(t, err)
310 assert.NotContains(t, manager.containers, "test.example.com")
311}
312
313func TestManager_RemoveContainerByDomain_NotFound(t *testing.T) {
314 cfg := &config.Config{}
315 manager := &Manager{
316 config: cfg,
317 containers: make(map[string]*runtime.Container),
318 }
319
320 ctx := testutils.TestContext(t)
321 err := manager.RemoveContainerByDomain(ctx, "nonexistent.example.com", false)
322
323 assert.Error(t, err)
324 assert.Contains(t, err.Error(), "no container found for domain")
325}
326
327func TestManager_GetContainerPort(t *testing.T) {
328 ctrl := gomock.NewController(t)
329 defer ctrl.Finish()
330
331 ctx := testutils.TestContext(t)
332 mockRuntime := mocks.NewMockRuntime(ctrl)
333
334 cfg := &config.Config{}
335 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
336 require.NoError(t, err)
337
338 container := &runtime.Container{
339 ID: "container123",
340 }
341 manager.containers["test.example.com"] = container
342
343 mockRuntime.EXPECT().GetContainerPort(ctx, "container123", 80).Return(8080, nil)
344
345 port, err := manager.GetContainerPort(ctx, "test.example.com", 80)
346
347 assert.NoError(t, err)
348 assert.Equal(t, 8080, port)
349}
350
351func TestManager_GetContainerPort_NotFound(t *testing.T) {
352 cfg := &config.Config{}
353 manager := &Manager{
354 config: cfg,
355 containers: make(map[string]*runtime.Container),
356 }
357
358 ctx := testutils.TestContext(t)
359 _, err := manager.GetContainerPort(ctx, "nonexistent.example.com", 80)
360
361 assert.Error(t, err)
362 assert.Contains(t, err.Error(), "no container found for domain")
363}
364
365func TestManager_FindContainerByDomain(t *testing.T) {
366 manager := &Manager{
367 containers: map[string]*runtime.Container{
368 "test.example.com": {ID: "container123"},
369 },
370 }
371
372 id, exists := manager.FindContainerByDomain("test.example.com")
373 assert.True(t, exists)
374 assert.Equal(t, "container123", id)
375
376 id, exists = manager.FindContainerByDomain("nonexistent.example.com")
377 assert.False(t, exists)
378 assert.Empty(t, id)
379}
380
381func TestManager_FindDomainByContainerID(t *testing.T) {
382 manager := &Manager{
383 containers: map[string]*runtime.Container{
384 "test.example.com": {ID: "container123"},
385 "app.example.com": {ID: "container456"},
386 },
387 }
388
389 domain, exists := manager.FindDomainByContainerID("container123")
390 assert.True(t, exists)
391 assert.Equal(t, "test.example.com", domain)
392
393 domain, exists = manager.FindDomainByContainerID("nonexistent")
394 assert.False(t, exists)
395 assert.Empty(t, domain)
396}
397
398func TestManager_StopAllManagedContainers(t *testing.T) {
399 ctrl := gomock.NewController(t)
400 defer ctrl.Finish()
401
402 ctx := testutils.TestContext(t)
403 mockRuntime := mocks.NewMockRuntime(ctrl)
404
405 cfg := &config.Config{}
406 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
407 require.NoError(t, err)
408
409 manager.containers["app1.example.com"] = &runtime.Container{ID: "container1"}
410 manager.containers["app2.example.com"] = &runtime.Container{ID: "container2"}
411
412 mockRuntime.EXPECT().StopContainer(ctx, "container1").Return(nil)
413 mockRuntime.EXPECT().StopContainer(ctx, "container2").Return(nil)
414
415 err = manager.StopAllManagedContainers(ctx)
416
417 assert.NoError(t, err)
418 assert.Empty(t, manager.containers)
419}
420
421func TestManager_StopAllManagedContainers_WithErrors(t *testing.T) {
422 ctrl := gomock.NewController(t)
423 defer ctrl.Finish()
424
425 ctx := testutils.TestContext(t)
426 mockRuntime := mocks.NewMockRuntime(ctrl)
427
428 cfg := &config.Config{}
429 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
430 require.NoError(t, err)
431
432 manager.containers["app1.example.com"] = &runtime.Container{ID: "container1"}
433 manager.containers["app2.example.com"] = &runtime.Container{ID: "container2"}
434
435 mockRuntime.EXPECT().StopContainer(ctx, "container1").Return(nil)
436 mockRuntime.EXPECT().StopContainer(ctx, "container2").Return(errors.New("stop failed"))
437
438 err = manager.StopAllManagedContainers(ctx)
439
440 assert.Error(t, err)
441 assert.Contains(t, err.Error(), "failed to stop 1 containers")
442}
443
444func TestManager_StopAllManagedContainers_Empty(t *testing.T) {
445 ctrl := gomock.NewController(t)
446 defer ctrl.Finish()
447
448 ctx := testutils.TestContext(t)
449 mockRuntime := mocks.NewMockRuntime(ctrl)
450
451 cfg := &config.Config{}
452 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
453 require.NoError(t, err)
454
455 err = manager.StopAllManagedContainers(ctx)
456
457 assert.NoError(t, err)
458 assert.Empty(t, manager.containers)
459}
460
461func TestManager_UpdateConfig(t *testing.T) {
462 ctrl := gomock.NewController(t)
463 defer ctrl.Finish()
464
465 mockRuntime := mocks.NewMockRuntime(ctrl)
466
467 originalCfg := &config.Config{
468 Server: config.ServerConfig{
469 Runtime: "docker",
470 },
471 }
472
473 manager, err := NewManagerWithRuntime(originalCfg, mockRuntime)
474 require.NoError(t, err)
475
476 newCfg := &config.Config{
477 Server: config.ServerConfig{
478 Runtime: "podman",
479 },
480 Routes: map[string]string{
481 "new-app.example.com": "nginx:latest",
482 },
483 }
484
485 manager.UpdateConfig(newCfg)
486
487 // Cleanup the env file created by UpdateConfig
488 defer func() {
489 if err := os.Remove("new-app_example_com.env"); err != nil && !os.IsNotExist(err) {
490 t.Logf("Failed to cleanup env file: %v", err)
491 }
492 }()
493
494 assert.Equal(t, newCfg, manager.config)
495 assert.Equal(t, "podman", manager.config.Server.Runtime)
496}
497
498func TestManager_Shutdown(t *testing.T) {
499 ctrl := gomock.NewController(t)
500 defer ctrl.Finish()
501
502 ctx := testutils.TestContext(t)
503 mockRuntime := mocks.NewMockRuntime(ctrl)
504
505 cfg := &config.Config{}
506 manager, err := NewManagerWithRuntime(cfg, mockRuntime)
507 require.NoError(t, err)
508
509 manager.containers["app1.example.com"] = &runtime.Container{ID: "container1"}
510
511 mockRuntime.EXPECT().StopContainer(ctx, "container1").Return(nil)
512
513 err = manager.Shutdown(ctx)
514
515 assert.NoError(t, err)
516 assert.Empty(t, manager.containers)
517}