1package config
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/zerowrap"
10 "github.com/spf13/viper"
11 "github.com/stretchr/testify/assert"
12 "github.com/stretchr/testify/require"
13
14 "github.com/bnema/gordon/internal/boundaries/out/mocks"
15 "github.com/bnema/gordon/internal/domain"
16)
17
18func testContext() context.Context {
19 return zerowrap.WithCtx(context.Background(), zerowrap.Default())
20}
21
22func TestService_Load(t *testing.T) {
23 v := viper.New()
24 v.Set("server.port", 8080)
25 v.Set("server.registry_port", 5000)
26 v.Set("server.registry_domain", "registry.example.com")
27 v.Set("server.data_dir", "/var/gordon")
28 v.Set("auto_route.enabled", true)
29 v.Set("network_isolation.enabled", true)
30 v.Set("network_isolation.network_prefix", "gordon")
31 v.Set("auth.enabled", true)
32 v.Set("auth.username", "admin")
33 v.Set("auth.password", "secret")
34 v.Set("volumes.auto_create", true)
35 v.Set("volumes.prefix", "gordon")
36 v.Set("volumes.preserve", false)
37 v.Set("routes", map[string]interface{}{
38 "app.example.com": "myapp:latest",
39 })
40
41 eventBus := mocks.NewMockEventPublisher(t)
42 svc := NewService(v, eventBus)
43 ctx := testContext()
44
45 err := svc.Load(ctx)
46
47 assert.NoError(t, err)
48 assert.Equal(t, 8080, svc.GetServerPort())
49 assert.Equal(t, 5000, svc.GetRegistryPort())
50 assert.Equal(t, "registry.example.com", svc.GetRegistryDomain())
51 assert.Equal(t, "/var/gordon", svc.GetDataDir())
52 assert.True(t, svc.IsAutoRouteEnabled())
53 assert.True(t, svc.IsNetworkIsolationEnabled())
54 assert.Equal(t, "gordon", svc.GetNetworkPrefix())
55
56 enabled, username, password := svc.GetRegistryAuthConfig()
57 assert.True(t, enabled)
58 assert.Equal(t, "admin", username)
59 assert.Equal(t, "secret", password)
60
61 autoCreate, prefix, preserve := svc.GetVolumeConfig()
62 assert.True(t, autoCreate)
63 assert.Equal(t, "gordon", prefix)
64 assert.False(t, preserve)
65}
66
67func TestService_Reload(t *testing.T) {
68 t.Run("success - picks up config file changes", func(t *testing.T) {
69 // Create temp config file
70 tmpDir := t.TempDir()
71 configFile := filepath.Join(tmpDir, "gordon.toml")
72 initialConfig := `[server]
73port = 8080
74
75[routes]
76"app.example.com" = "myapp:v1"
77`
78 err := os.WriteFile(configFile, []byte(initialConfig), 0600)
79 require.NoError(t, err)
80
81 v := viper.New()
82 v.SetConfigFile(configFile)
83 err = v.ReadInConfig()
84 require.NoError(t, err)
85
86 eventBus := mocks.NewMockEventPublisher(t)
87 svc := NewService(v, eventBus)
88 ctx := testContext()
89
90 // Initial load
91 err = svc.Load(ctx)
92 require.NoError(t, err)
93 assert.Equal(t, 8080, svc.GetServerPort())
94 assert.Equal(t, "myapp:v1", svc.GetConfig().Routes["app.example.com"])
95
96 // Modify config file on disk
97 updatedConfig := `[server]
98port = 9090
99
100[routes]
101"app.example.com" = "myapp:v2"
102"new.example.com" = "newapp:latest"
103`
104 err = os.WriteFile(configFile, []byte(updatedConfig), 0600)
105 require.NoError(t, err)
106
107 // Reload should pick up new values
108 err = svc.Reload(ctx)
109 require.NoError(t, err)
110
111 assert.Equal(t, 9090, svc.GetServerPort())
112 assert.Equal(t, "myapp:v2", svc.GetConfig().Routes["app.example.com"])
113 assert.Equal(t, "newapp:latest", svc.GetConfig().Routes["new.example.com"])
114 })
115
116 t.Run("error - config file not found", func(t *testing.T) {
117 tmpDir := t.TempDir()
118 configFile := filepath.Join(tmpDir, "gordon.toml")
119
120 // Create config, load it, then delete it
121 err := os.WriteFile(configFile, []byte("[server]\nport = 8080\n"), 0600)
122 require.NoError(t, err)
123
124 v := viper.New()
125 v.SetConfigFile(configFile)
126 err = v.ReadInConfig()
127 require.NoError(t, err)
128
129 eventBus := mocks.NewMockEventPublisher(t)
130 svc := NewService(v, eventBus)
131 ctx := testContext()
132
133 err = svc.Load(ctx)
134 require.NoError(t, err)
135
136 // Delete the config file
137 err = os.Remove(configFile)
138 require.NoError(t, err)
139
140 // Reload should fail
141 err = svc.Reload(ctx)
142 assert.Error(t, err)
143 assert.Contains(t, err.Error(), "failed to read config file")
144 })
145
146 t.Run("error - invalid config syntax", func(t *testing.T) {
147 tmpDir := t.TempDir()
148 configFile := filepath.Join(tmpDir, "gordon.toml")
149
150 // Create valid config initially
151 err := os.WriteFile(configFile, []byte("[server]\nport = 8080\n"), 0600)
152 require.NoError(t, err)
153
154 v := viper.New()
155 v.SetConfigFile(configFile)
156 err = v.ReadInConfig()
157 require.NoError(t, err)
158
159 eventBus := mocks.NewMockEventPublisher(t)
160 svc := NewService(v, eventBus)
161 ctx := testContext()
162
163 err = svc.Load(ctx)
164 require.NoError(t, err)
165
166 // Write invalid TOML
167 err = os.WriteFile(configFile, []byte("[server\nport = invalid syntax"), 0600)
168 require.NoError(t, err)
169
170 // Reload should fail
171 err = svc.Reload(ctx)
172 assert.Error(t, err)
173 })
174}
175
176func TestService_GetRoutes(t *testing.T) {
177 v := viper.New()
178 v.Set("routes", map[string]interface{}{
179 "app1.example.com": "myapp:latest",
180 "app2.example.com": "otherapp:v2",
181 "http://insecure.example": "insecureapp:latest",
182 })
183
184 eventBus := mocks.NewMockEventPublisher(t)
185 svc := NewService(v, eventBus)
186 ctx := testContext()
187
188 _ = svc.Load(ctx)
189
190 routes := svc.GetRoutes(ctx)
191
192 assert.Len(t, routes, 3)
193
194 // Check routes (order is not guaranteed)
195 routeMap := make(map[string]domain.Route)
196 for _, r := range routes {
197 routeMap[r.Domain] = r
198 }
199
200 assert.Equal(t, "myapp:latest", routeMap["app1.example.com"].Image)
201 assert.True(t, routeMap["app1.example.com"].HTTPS)
202
203 assert.Equal(t, "otherapp:v2", routeMap["app2.example.com"].Image)
204 assert.True(t, routeMap["app2.example.com"].HTTPS)
205
206 assert.Equal(t, "insecureapp:latest", routeMap["insecure.example"].Image)
207 assert.False(t, routeMap["insecure.example"].HTTPS)
208}
209
210func TestService_GetRoute(t *testing.T) {
211 v := viper.New()
212 v.Set("routes", map[string]interface{}{
213 "app.example.com": "myapp:latest",
214 "http://insecure.example": "insecureapp:latest",
215 })
216
217 eventBus := mocks.NewMockEventPublisher(t)
218 svc := NewService(v, eventBus)
219 ctx := testContext()
220
221 _ = svc.Load(ctx)
222
223 t.Run("existing route", func(t *testing.T) {
224 route, err := svc.GetRoute(ctx, "app.example.com")
225 require.NoError(t, err)
226 assert.Equal(t, "app.example.com", route.Domain)
227 assert.Equal(t, "myapp:latest", route.Image)
228 assert.True(t, route.HTTPS)
229 })
230
231 t.Run("non-existent route", func(t *testing.T) {
232 route, err := svc.GetRoute(ctx, "notfound.example.com")
233 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
234 assert.Nil(t, route)
235 })
236}
237
238func TestService_AddRoute(t *testing.T) {
239 t.Run("success", func(t *testing.T) {
240 // Create temp config file for Save() to work
241 tmpDir := t.TempDir()
242 configFile := filepath.Join(tmpDir, "gordon.toml")
243 err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
244 require.NoError(t, err)
245
246 v := viper.New()
247 v.SetConfigFile(configFile)
248 eventBus := mocks.NewMockEventPublisher(t)
249 svc := NewService(v, eventBus)
250 ctx := testContext()
251
252 _ = svc.Load(ctx)
253
254 route := domain.Route{
255 Domain: "new.example.com",
256 Image: "newapp:latest",
257 }
258
259 err = svc.AddRoute(ctx, route)
260
261 assert.NoError(t, err)
262
263 // Verify route was added
264 config := svc.GetConfig()
265 assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
266 })
267
268 t.Run("empty domain", func(t *testing.T) {
269 v := viper.New()
270 eventBus := mocks.NewMockEventPublisher(t)
271 svc := NewService(v, eventBus)
272 ctx := testContext()
273
274 route := domain.Route{
275 Domain: "",
276 Image: "myapp:latest",
277 }
278
279 err := svc.AddRoute(ctx, route)
280 assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
281 })
282
283 t.Run("empty image", func(t *testing.T) {
284 v := viper.New()
285 eventBus := mocks.NewMockEventPublisher(t)
286 svc := NewService(v, eventBus)
287 ctx := testContext()
288
289 route := domain.Route{
290 Domain: "example.com",
291 Image: "",
292 }
293
294 err := svc.AddRoute(ctx, route)
295 assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
296 })
297}
298
299func TestService_UpdateRoute(t *testing.T) {
300 t.Run("success", func(t *testing.T) {
301 // Create temp config file for Save() to work
302 tmpDir := t.TempDir()
303 configFile := filepath.Join(tmpDir, "gordon.toml")
304 err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
305 require.NoError(t, err)
306
307 v := viper.New()
308 v.SetConfigFile(configFile)
309 v.Set("routes", map[string]any{
310 "app.example.com": "myapp:v1",
311 })
312
313 eventBus := mocks.NewMockEventPublisher(t)
314 svc := NewService(v, eventBus)
315 ctx := testContext()
316
317 _ = svc.Load(ctx)
318
319 route := domain.Route{
320 Domain: "app.example.com",
321 Image: "myapp:v2",
322 }
323
324 err = svc.UpdateRoute(ctx, route)
325
326 assert.NoError(t, err)
327
328 config := svc.GetConfig()
329 assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
330 })
331
332 t.Run("not found", func(t *testing.T) {
333 v := viper.New()
334 eventBus := mocks.NewMockEventPublisher(t)
335 svc := NewService(v, eventBus)
336 ctx := testContext()
337
338 _ = svc.Load(ctx)
339
340 route := domain.Route{
341 Domain: "nonexistent.example.com",
342 Image: "myapp:latest",
343 }
344
345 err := svc.UpdateRoute(ctx, route)
346 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
347 })
348
349 t.Run("empty domain", func(t *testing.T) {
350 v := viper.New()
351 eventBus := mocks.NewMockEventPublisher(t)
352 svc := NewService(v, eventBus)
353 ctx := testContext()
354
355 route := domain.Route{
356 Domain: "",
357 Image: "myapp:latest",
358 }
359
360 err := svc.UpdateRoute(ctx, route)
361 assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
362 })
363
364 t.Run("empty image", func(t *testing.T) {
365 v := viper.New()
366 eventBus := mocks.NewMockEventPublisher(t)
367 svc := NewService(v, eventBus)
368 ctx := testContext()
369
370 route := domain.Route{
371 Domain: "example.com",
372 Image: "",
373 }
374
375 err := svc.UpdateRoute(ctx, route)
376 assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
377 })
378}
379
380func TestService_RemoveRoute(t *testing.T) {
381 // Create temp config file for Save() to work
382 tmpDir := t.TempDir()
383 configFile := filepath.Join(tmpDir, "gordon.toml")
384 err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:latest\"\n"), 0600)
385 require.NoError(t, err)
386
387 v := viper.New()
388 v.SetConfigFile(configFile)
389 v.Set("routes", map[string]interface{}{
390 "app.example.com": "myapp:latest",
391 })
392
393 eventBus := mocks.NewMockEventPublisher(t)
394 svc := NewService(v, eventBus)
395 ctx := testContext()
396
397 _ = svc.Load(ctx)
398
399 err = svc.RemoveRoute(ctx, "app.example.com")
400
401 assert.NoError(t, err)
402
403 config := svc.GetConfig()
404 _, exists := config.Routes["app.example.com"]
405 assert.False(t, exists)
406}
407
408func TestService_RemoveRoute_NotFound(t *testing.T) {
409 v := viper.New()
410 eventBus := mocks.NewMockEventPublisher(t)
411 svc := NewService(v, eventBus)
412 ctx := testContext()
413
414 _ = svc.Load(ctx)
415
416 err := svc.RemoveRoute(ctx, "nonexistent.example.com")
417
418 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
419}
420
421func TestService_GetNetworkGroups(t *testing.T) {
422 v := viper.New()
423 v.Set("network_groups", map[string]interface{}{
424 "frontend": []interface{}{"app1.example.com", "app2.example.com"},
425 "backend": []interface{}{"api.example.com", "db.example.com"},
426 })
427
428 eventBus := mocks.NewMockEventPublisher(t)
429 svc := NewService(v, eventBus)
430 ctx := testContext()
431
432 _ = svc.Load(ctx)
433
434 groups := svc.GetNetworkGroups()
435
436 assert.Len(t, groups, 2)
437 assert.ElementsMatch(t, []string{"app1.example.com", "app2.example.com"}, groups["frontend"])
438 assert.ElementsMatch(t, []string{"api.example.com", "db.example.com"}, groups["backend"])
439}
440
441func TestService_GetAttachments(t *testing.T) {
442 v := viper.New()
443 v.Set("attachments", map[string]interface{}{
444 "app.example.com": []interface{}{"redis:latest", "postgres:18"},
445 })
446
447 eventBus := mocks.NewMockEventPublisher(t)
448 svc := NewService(v, eventBus)
449 ctx := testContext()
450
451 _ = svc.Load(ctx)
452
453 attachments := svc.GetAttachments()
454
455 assert.Len(t, attachments, 1)
456 assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"])
457}
458
459func TestService_GetAllAttachments(t *testing.T) {
460 t.Run("returns all attachments", func(t *testing.T) {
461 v := viper.New()
462 v.Set("attachments", map[string]interface{}{
463 "app.example.com": []interface{}{"redis:latest", "postgres:18"},
464 "api.example.com": []interface{}{"rabbitmq:3"},
465 })
466
467 eventBus := mocks.NewMockEventPublisher(t)
468 svc := NewService(v, eventBus)
469 ctx := testContext()
470
471 _ = svc.Load(ctx)
472
473 attachments := svc.GetAllAttachments(ctx)
474
475 assert.Len(t, attachments, 2)
476 assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, attachments["app.example.com"])
477 assert.ElementsMatch(t, []string{"rabbitmq:3"}, attachments["api.example.com"])
478 })
479
480 t.Run("returns empty map when no attachments", func(t *testing.T) {
481 v := viper.New()
482 eventBus := mocks.NewMockEventPublisher(t)
483 svc := NewService(v, eventBus)
484 ctx := testContext()
485
486 _ = svc.Load(ctx)
487
488 attachments := svc.GetAllAttachments(ctx)
489
490 assert.Empty(t, attachments)
491 })
492
493 t.Run("returns copy not reference", func(t *testing.T) {
494 v := viper.New()
495 v.Set("attachments", map[string]interface{}{
496 "app.example.com": []interface{}{"redis:latest"},
497 })
498
499 eventBus := mocks.NewMockEventPublisher(t)
500 svc := NewService(v, eventBus)
501 ctx := testContext()
502
503 _ = svc.Load(ctx)
504
505 attachments := svc.GetAllAttachments(ctx)
506 // Modify the returned map
507 attachments["app.example.com"] = append(attachments["app.example.com"], "postgres:18")
508
509 // Original should be unchanged
510 original := svc.GetAllAttachments(ctx)
511 assert.Len(t, original["app.example.com"], 1)
512 })
513}
514
515func TestService_GetAttachmentsFor(t *testing.T) {
516 t.Run("existing domain", func(t *testing.T) {
517 v := viper.New()
518 v.Set("attachments", map[string]interface{}{
519 "app.example.com": []interface{}{"redis:latest", "postgres:18"},
520 })
521
522 eventBus := mocks.NewMockEventPublisher(t)
523 svc := NewService(v, eventBus)
524 ctx := testContext()
525
526 _ = svc.Load(ctx)
527
528 images, err := svc.GetAttachmentsFor(ctx, "app.example.com")
529
530 require.NoError(t, err)
531 assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, images)
532 })
533
534 t.Run("existing network group", func(t *testing.T) {
535 v := viper.New()
536 v.Set("attachments", map[string]interface{}{
537 "backend": []interface{}{"rabbitmq:3", "redis:latest"},
538 })
539
540 eventBus := mocks.NewMockEventPublisher(t)
541 svc := NewService(v, eventBus)
542 ctx := testContext()
543
544 _ = svc.Load(ctx)
545
546 images, err := svc.GetAttachmentsFor(ctx, "backend")
547
548 require.NoError(t, err)
549 assert.ElementsMatch(t, []string{"rabbitmq:3", "redis:latest"}, images)
550 })
551
552 t.Run("non-existent target", func(t *testing.T) {
553 v := viper.New()
554 eventBus := mocks.NewMockEventPublisher(t)
555 svc := NewService(v, eventBus)
556 ctx := testContext()
557
558 _ = svc.Load(ctx)
559
560 images, err := svc.GetAttachmentsFor(ctx, "notfound.example.com")
561
562 assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
563 assert.Nil(t, images)
564 })
565
566 t.Run("returns copy not reference", func(t *testing.T) {
567 v := viper.New()
568 v.Set("attachments", map[string]interface{}{
569 "app.example.com": []interface{}{"redis:latest"},
570 })
571
572 eventBus := mocks.NewMockEventPublisher(t)
573 svc := NewService(v, eventBus)
574 ctx := testContext()
575
576 _ = svc.Load(ctx)
577
578 images, err := svc.GetAttachmentsFor(ctx, "app.example.com")
579 require.NoError(t, err)
580
581 // Modify the returned slice (use _ to satisfy linter)
582 _ = append(images, "postgres:18")
583
584 // Original should be unchanged
585 original, _ := svc.GetAttachmentsFor(ctx, "app.example.com")
586 assert.Len(t, original, 1)
587 })
588}
589
590func TestService_AddAttachment(t *testing.T) {
591 t.Run("success - new target", func(t *testing.T) {
592 tmpDir := t.TempDir()
593 configFile := filepath.Join(tmpDir, "gordon.toml")
594 err := os.WriteFile(configFile, []byte("[attachments]\n"), 0600)
595 require.NoError(t, err)
596
597 v := viper.New()
598 v.SetConfigFile(configFile)
599 eventBus := mocks.NewMockEventPublisher(t)
600 svc := NewService(v, eventBus)
601 ctx := testContext()
602
603 _ = svc.Load(ctx)
604
605 err = svc.AddAttachment(ctx, "app.example.com", "postgres:18")
606
607 assert.NoError(t, err)
608
609 // Verify attachment was added
610 config := svc.GetConfig()
611 assert.Contains(t, config.Attachments["app.example.com"], "postgres:18")
612 })
613
614 t.Run("success - existing target", func(t *testing.T) {
615 tmpDir := t.TempDir()
616 configFile := filepath.Join(tmpDir, "gordon.toml")
617 err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\"]\n"), 0600)
618 require.NoError(t, err)
619
620 v := viper.New()
621 v.SetConfigFile(configFile)
622 v.Set("attachments", map[string]interface{}{
623 "app.example.com": []interface{}{"redis:latest"},
624 })
625 eventBus := mocks.NewMockEventPublisher(t)
626 svc := NewService(v, eventBus)
627 ctx := testContext()
628
629 _ = svc.Load(ctx)
630
631 err = svc.AddAttachment(ctx, "app.example.com", "postgres:18")
632
633 assert.NoError(t, err)
634
635 // Verify attachment was added alongside existing
636 config := svc.GetConfig()
637 assert.ElementsMatch(t, []string{"redis:latest", "postgres:18"}, config.Attachments["app.example.com"])
638 })
639
640 t.Run("duplicate attachment", func(t *testing.T) {
641 v := viper.New()
642 v.Set("attachments", map[string]interface{}{
643 "app.example.com": []interface{}{"redis:latest"},
644 })
645 eventBus := mocks.NewMockEventPublisher(t)
646 svc := NewService(v, eventBus)
647 ctx := testContext()
648
649 _ = svc.Load(ctx)
650
651 err := svc.AddAttachment(ctx, "app.example.com", "redis:latest")
652
653 assert.ErrorIs(t, err, domain.ErrAttachmentExists)
654 })
655
656 t.Run("empty target", func(t *testing.T) {
657 v := viper.New()
658 eventBus := mocks.NewMockEventPublisher(t)
659 svc := NewService(v, eventBus)
660 ctx := testContext()
661
662 err := svc.AddAttachment(ctx, "", "postgres:18")
663
664 assert.ErrorIs(t, err, domain.ErrAttachmentTargetEmpty)
665 })
666
667 t.Run("empty image", func(t *testing.T) {
668 v := viper.New()
669 eventBus := mocks.NewMockEventPublisher(t)
670 svc := NewService(v, eventBus)
671 ctx := testContext()
672
673 err := svc.AddAttachment(ctx, "app.example.com", "")
674
675 assert.ErrorIs(t, err, domain.ErrAttachmentImageEmpty)
676 })
677
678 t.Run("network group target", func(t *testing.T) {
679 tmpDir := t.TempDir()
680 configFile := filepath.Join(tmpDir, "gordon.toml")
681 err := os.WriteFile(configFile, []byte("[attachments]\n"), 0600)
682 require.NoError(t, err)
683
684 v := viper.New()
685 v.SetConfigFile(configFile)
686 eventBus := mocks.NewMockEventPublisher(t)
687 svc := NewService(v, eventBus)
688 ctx := testContext()
689
690 _ = svc.Load(ctx)
691
692 err = svc.AddAttachment(ctx, "backend", "rabbitmq:3")
693
694 assert.NoError(t, err)
695
696 config := svc.GetConfig()
697 assert.Contains(t, config.Attachments["backend"], "rabbitmq:3")
698 })
699}
700
701func TestService_RemoveAttachment(t *testing.T) {
702 t.Run("success - single attachment", func(t *testing.T) {
703 tmpDir := t.TempDir()
704 configFile := filepath.Join(tmpDir, "gordon.toml")
705 err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\"]\n"), 0600)
706 require.NoError(t, err)
707
708 v := viper.New()
709 v.SetConfigFile(configFile)
710 v.Set("attachments", map[string]interface{}{
711 "app.example.com": []interface{}{"redis:latest"},
712 })
713 eventBus := mocks.NewMockEventPublisher(t)
714 svc := NewService(v, eventBus)
715 ctx := testContext()
716
717 _ = svc.Load(ctx)
718
719 err = svc.RemoveAttachment(ctx, "app.example.com", "redis:latest")
720
721 assert.NoError(t, err)
722
723 // Verify target was removed (no attachments left)
724 config := svc.GetConfig()
725 _, exists := config.Attachments["app.example.com"]
726 assert.False(t, exists)
727 })
728
729 t.Run("success - multiple attachments", func(t *testing.T) {
730 tmpDir := t.TempDir()
731 configFile := filepath.Join(tmpDir, "gordon.toml")
732 err := os.WriteFile(configFile, []byte("[attachments]\n\"app.example.com\" = [\"redis:latest\", \"postgres:18\"]\n"), 0600)
733 require.NoError(t, err)
734
735 v := viper.New()
736 v.SetConfigFile(configFile)
737 v.Set("attachments", map[string]interface{}{
738 "app.example.com": []interface{}{"redis:latest", "postgres:18"},
739 })
740 eventBus := mocks.NewMockEventPublisher(t)
741 svc := NewService(v, eventBus)
742 ctx := testContext()
743
744 _ = svc.Load(ctx)
745
746 err = svc.RemoveAttachment(ctx, "app.example.com", "redis:latest")
747
748 assert.NoError(t, err)
749
750 // Verify only redis was removed
751 config := svc.GetConfig()
752 assert.Equal(t, []string{"postgres:18"}, config.Attachments["app.example.com"])
753 })
754
755 t.Run("target not found", func(t *testing.T) {
756 v := viper.New()
757 eventBus := mocks.NewMockEventPublisher(t)
758 svc := NewService(v, eventBus)
759 ctx := testContext()
760
761 _ = svc.Load(ctx)
762
763 err := svc.RemoveAttachment(ctx, "notfound.example.com", "redis:latest")
764
765 assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
766 })
767
768 t.Run("image not found in target", func(t *testing.T) {
769 v := viper.New()
770 v.Set("attachments", map[string]interface{}{
771 "app.example.com": []interface{}{"redis:latest"},
772 })
773 eventBus := mocks.NewMockEventPublisher(t)
774 svc := NewService(v, eventBus)
775 ctx := testContext()
776
777 _ = svc.Load(ctx)
778
779 err := svc.RemoveAttachment(ctx, "app.example.com", "postgres:18")
780
781 assert.ErrorIs(t, err, domain.ErrAttachmentNotFound)
782 })
783
784 t.Run("empty target", func(t *testing.T) {
785 v := viper.New()
786 eventBus := mocks.NewMockEventPublisher(t)
787 svc := NewService(v, eventBus)
788 ctx := testContext()
789
790 err := svc.RemoveAttachment(ctx, "", "redis:latest")
791
792 assert.ErrorIs(t, err, domain.ErrAttachmentTargetEmpty)
793 })
794
795 t.Run("empty image", func(t *testing.T) {
796 v := viper.New()
797 eventBus := mocks.NewMockEventPublisher(t)
798 svc := NewService(v, eventBus)
799 ctx := testContext()
800
801 err := svc.RemoveAttachment(ctx, "app.example.com", "")
802
803 assert.ErrorIs(t, err, domain.ErrAttachmentImageEmpty)
804 })
805
806 t.Run("network group target", func(t *testing.T) {
807 tmpDir := t.TempDir()
808 configFile := filepath.Join(tmpDir, "gordon.toml")
809 err := os.WriteFile(configFile, []byte("[attachments]\n\"backend\" = [\"rabbitmq:3\"]\n"), 0600)
810 require.NoError(t, err)
811
812 v := viper.New()
813 v.SetConfigFile(configFile)
814 v.Set("attachments", map[string]interface{}{
815 "backend": []interface{}{"rabbitmq:3"},
816 })
817 eventBus := mocks.NewMockEventPublisher(t)
818 svc := NewService(v, eventBus)
819 ctx := testContext()
820
821 _ = svc.Load(ctx)
822
823 err = svc.RemoveAttachment(ctx, "backend", "rabbitmq:3")
824
825 assert.NoError(t, err)
826
827 config := svc.GetConfig()
828 _, exists := config.Attachments["backend"]
829 assert.False(t, exists)
830 })
831}
832
833func TestService_GetExternalRoutes(t *testing.T) {
834 v := viper.New()
835 v.Set("external_routes", map[string]interface{}{
836 "reg.example.com": "localhost:5000",
837 "cache.example.com": "127.0.0.1:6379",
838 })
839
840 eventBus := mocks.NewMockEventPublisher(t)
841 svc := NewService(v, eventBus)
842 ctx := testContext()
843
844 _ = svc.Load(ctx)
845
846 routes := svc.GetExternalRoutes()
847
848 assert.Len(t, routes, 2)
849 assert.Equal(t, "localhost:5000", routes["reg.example.com"])
850 assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
851}
852
853func TestService_GetExternalRoutes_Empty(t *testing.T) {
854 v := viper.New()
855
856 eventBus := mocks.NewMockEventPublisher(t)
857 svc := NewService(v, eventBus)
858 ctx := testContext()
859
860 _ = svc.Load(ctx)
861
862 routes := svc.GetExternalRoutes()
863
864 assert.Empty(t, routes)
865}
866
867func TestExtractDomainFromImageName(t *testing.T) {
868 tests := []struct {
869 name string
870 imageName string
871 expectedDom string
872 expectedFound bool
873 }{
874 {
875 name: "domain like image",
876 imageName: "myapp.example.com:latest",
877 expectedDom: "myapp.example.com",
878 expectedFound: true,
879 },
880 {
881 name: "domain like image without tag",
882 imageName: "api.backend.io",
883 expectedDom: "api.backend.io",
884 expectedFound: true,
885 },
886 {
887 name: "simple image name",
888 imageName: "nginx:latest",
889 expectedDom: "",
890 expectedFound: false,
891 },
892 {
893 name: "registry path",
894 imageName: "gcr.io/project/image:tag",
895 expectedDom: "gcr.io/project/image",
896 expectedFound: true,
897 },
898 {
899 name: "leading dot",
900 imageName: ".invalid:latest",
901 expectedDom: "",
902 expectedFound: false,
903 },
904 {
905 name: "trailing dot",
906 imageName: "invalid.:latest",
907 expectedDom: "",
908 expectedFound: false,
909 },
910 }
911
912 for _, tt := range tests {
913 t.Run(tt.name, func(t *testing.T) {
914 domain, found := ExtractDomainFromImageName(tt.imageName)
915 assert.Equal(t, tt.expectedDom, domain)
916 assert.Equal(t, tt.expectedFound, found)
917 })
918 }
919}
920
921func TestLoadStringMap(t *testing.T) {
922 tests := []struct {
923 name string
924 input any
925 expected map[string]string
926 }{
927 {
928 name: "nil input",
929 input: nil,
930 expected: map[string]string{},
931 },
932 {
933 name: "valid map",
934 input: map[string]any{
935 "key1": "value1",
936 "key2": "value2",
937 },
938 expected: map[string]string{
939 "key1": "value1",
940 "key2": "value2",
941 },
942 },
943 {
944 name: "invalid type",
945 input: "not a map",
946 expected: map[string]string{},
947 },
948 {
949 name: "mixed values",
950 input: map[string]any{
951 "string": "value",
952 "int": 123,
953 },
954 expected: map[string]string{
955 "string": "value",
956 },
957 },
958 }
959
960 for _, tt := range tests {
961 t.Run(tt.name, func(t *testing.T) {
962 result := loadStringMap(tt.input)
963 assert.Equal(t, tt.expected, result)
964 })
965 }
966}
967
968func TestLoadStringArrayMap(t *testing.T) {
969 tests := []struct {
970 name string
971 input any
972 expected map[string][]string
973 }{
974 {
975 name: "nil input",
976 input: nil,
977 expected: map[string][]string{},
978 },
979 {
980 name: "valid map",
981 input: map[string]any{
982 "group1": []any{"a", "b"},
983 "group2": []any{"c", "d", "e"},
984 },
985 expected: map[string][]string{
986 "group1": {"a", "b"},
987 "group2": {"c", "d", "e"},
988 },
989 },
990 {
991 name: "invalid type",
992 input: "not a map",
993 expected: map[string][]string{},
994 },
995 {
996 name: "non-array value",
997 input: map[string]any{
998 "array": []any{"a", "b"},
999 "string": "not an array",
1000 },
1001 expected: map[string][]string{
1002 "array": {"a", "b"},
1003 },
1004 },
1005 }
1006
1007 for _, tt := range tests {
1008 t.Run(tt.name, func(t *testing.T) {
1009 result := loadStringArrayMap(tt.input)
1010 assert.Equal(t, tt.expected, result)
1011 })
1012 }
1013}