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 "gordon/internal/boundaries/out/mocks"
15 "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("registry_auth.enabled", true)
32 v.Set("registry_auth.username", "admin")
33 v.Set("registry_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_GetRoutes(t *testing.T) {
68 v := viper.New()
69 v.Set("routes", map[string]interface{}{
70 "app1.example.com": "myapp:latest",
71 "app2.example.com": "otherapp:v2",
72 "http://insecure.example": "insecureapp:latest",
73 })
74
75 eventBus := mocks.NewMockEventPublisher(t)
76 svc := NewService(v, eventBus)
77 ctx := testContext()
78
79 _ = svc.Load(ctx)
80
81 routes := svc.GetRoutes(ctx)
82
83 assert.Len(t, routes, 3)
84
85 // Check routes (order is not guaranteed)
86 routeMap := make(map[string]domain.Route)
87 for _, r := range routes {
88 routeMap[r.Domain] = r
89 }
90
91 assert.Equal(t, "myapp:latest", routeMap["app1.example.com"].Image)
92 assert.True(t, routeMap["app1.example.com"].HTTPS)
93
94 assert.Equal(t, "otherapp:v2", routeMap["app2.example.com"].Image)
95 assert.True(t, routeMap["app2.example.com"].HTTPS)
96
97 assert.Equal(t, "insecureapp:latest", routeMap["insecure.example"].Image)
98 assert.False(t, routeMap["insecure.example"].HTTPS)
99}
100
101func TestService_GetRoute(t *testing.T) {
102 v := viper.New()
103 v.Set("routes", map[string]interface{}{
104 "app.example.com": "myapp:latest",
105 "http://insecure.example": "insecureapp:latest",
106 })
107
108 eventBus := mocks.NewMockEventPublisher(t)
109 svc := NewService(v, eventBus)
110 ctx := testContext()
111
112 _ = svc.Load(ctx)
113
114 t.Run("existing route", func(t *testing.T) {
115 route, err := svc.GetRoute(ctx, "app.example.com")
116 require.NoError(t, err)
117 assert.Equal(t, "app.example.com", route.Domain)
118 assert.Equal(t, "myapp:latest", route.Image)
119 assert.True(t, route.HTTPS)
120 })
121
122 t.Run("non-existent route", func(t *testing.T) {
123 route, err := svc.GetRoute(ctx, "notfound.example.com")
124 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
125 assert.Nil(t, route)
126 })
127}
128
129func TestService_AddRoute(t *testing.T) {
130 t.Run("success", func(t *testing.T) {
131 // Create temp config file for Save() to work
132 tmpDir := t.TempDir()
133 configFile := filepath.Join(tmpDir, "gordon.toml")
134 err := os.WriteFile(configFile, []byte("[routes]\n"), 0600)
135 require.NoError(t, err)
136
137 v := viper.New()
138 v.SetConfigFile(configFile)
139 eventBus := mocks.NewMockEventPublisher(t)
140 svc := NewService(v, eventBus)
141 ctx := testContext()
142
143 _ = svc.Load(ctx)
144
145 route := domain.Route{
146 Domain: "new.example.com",
147 Image: "newapp:latest",
148 }
149
150 err = svc.AddRoute(ctx, route)
151
152 assert.NoError(t, err)
153
154 // Verify route was added
155 config := svc.GetConfig()
156 assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
157 })
158
159 t.Run("empty domain", func(t *testing.T) {
160 v := viper.New()
161 eventBus := mocks.NewMockEventPublisher(t)
162 svc := NewService(v, eventBus)
163 ctx := testContext()
164
165 route := domain.Route{
166 Domain: "",
167 Image: "myapp:latest",
168 }
169
170 err := svc.AddRoute(ctx, route)
171 assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
172 })
173
174 t.Run("empty image", func(t *testing.T) {
175 v := viper.New()
176 eventBus := mocks.NewMockEventPublisher(t)
177 svc := NewService(v, eventBus)
178 ctx := testContext()
179
180 route := domain.Route{
181 Domain: "example.com",
182 Image: "",
183 }
184
185 err := svc.AddRoute(ctx, route)
186 assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
187 })
188}
189
190func TestService_UpdateRoute(t *testing.T) {
191 t.Run("success", func(t *testing.T) {
192 // Create temp config file for Save() to work
193 tmpDir := t.TempDir()
194 configFile := filepath.Join(tmpDir, "gordon.toml")
195 err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:v1\"\n"), 0600)
196 require.NoError(t, err)
197
198 v := viper.New()
199 v.SetConfigFile(configFile)
200 v.Set("routes", map[string]any{
201 "app.example.com": "myapp:v1",
202 })
203
204 eventBus := mocks.NewMockEventPublisher(t)
205 svc := NewService(v, eventBus)
206 ctx := testContext()
207
208 _ = svc.Load(ctx)
209
210 route := domain.Route{
211 Domain: "app.example.com",
212 Image: "myapp:v2",
213 }
214
215 err = svc.UpdateRoute(ctx, route)
216
217 assert.NoError(t, err)
218
219 config := svc.GetConfig()
220 assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
221 })
222
223 t.Run("not found", func(t *testing.T) {
224 v := viper.New()
225 eventBus := mocks.NewMockEventPublisher(t)
226 svc := NewService(v, eventBus)
227 ctx := testContext()
228
229 _ = svc.Load(ctx)
230
231 route := domain.Route{
232 Domain: "nonexistent.example.com",
233 Image: "myapp:latest",
234 }
235
236 err := svc.UpdateRoute(ctx, route)
237 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
238 })
239
240 t.Run("empty domain", func(t *testing.T) {
241 v := viper.New()
242 eventBus := mocks.NewMockEventPublisher(t)
243 svc := NewService(v, eventBus)
244 ctx := testContext()
245
246 route := domain.Route{
247 Domain: "",
248 Image: "myapp:latest",
249 }
250
251 err := svc.UpdateRoute(ctx, route)
252 assert.ErrorIs(t, err, domain.ErrRouteDomainEmpty)
253 })
254
255 t.Run("empty image", func(t *testing.T) {
256 v := viper.New()
257 eventBus := mocks.NewMockEventPublisher(t)
258 svc := NewService(v, eventBus)
259 ctx := testContext()
260
261 route := domain.Route{
262 Domain: "example.com",
263 Image: "",
264 }
265
266 err := svc.UpdateRoute(ctx, route)
267 assert.ErrorIs(t, err, domain.ErrRouteImageEmpty)
268 })
269}
270
271func TestService_RemoveRoute(t *testing.T) {
272 // Create temp config file for Save() to work
273 tmpDir := t.TempDir()
274 configFile := filepath.Join(tmpDir, "gordon.toml")
275 err := os.WriteFile(configFile, []byte("[routes]\n\"app.example.com\" = \"myapp:latest\"\n"), 0600)
276 require.NoError(t, err)
277
278 v := viper.New()
279 v.SetConfigFile(configFile)
280 v.Set("routes", map[string]interface{}{
281 "app.example.com": "myapp:latest",
282 })
283
284 eventBus := mocks.NewMockEventPublisher(t)
285 svc := NewService(v, eventBus)
286 ctx := testContext()
287
288 _ = svc.Load(ctx)
289
290 err = svc.RemoveRoute(ctx, "app.example.com")
291
292 assert.NoError(t, err)
293
294 config := svc.GetConfig()
295 _, exists := config.Routes["app.example.com"]
296 assert.False(t, exists)
297}
298
299func TestService_RemoveRoute_NotFound(t *testing.T) {
300 v := viper.New()
301 eventBus := mocks.NewMockEventPublisher(t)
302 svc := NewService(v, eventBus)
303 ctx := testContext()
304
305 _ = svc.Load(ctx)
306
307 err := svc.RemoveRoute(ctx, "nonexistent.example.com")
308
309 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
310}
311
312func TestService_GetNetworkGroups(t *testing.T) {
313 v := viper.New()
314 v.Set("network_groups", map[string]interface{}{
315 "frontend": []interface{}{"app1.example.com", "app2.example.com"},
316 "backend": []interface{}{"api.example.com", "db.example.com"},
317 })
318
319 eventBus := mocks.NewMockEventPublisher(t)
320 svc := NewService(v, eventBus)
321 ctx := testContext()
322
323 _ = svc.Load(ctx)
324
325 groups := svc.GetNetworkGroups()
326
327 assert.Len(t, groups, 2)
328 assert.ElementsMatch(t, []string{"app1.example.com", "app2.example.com"}, groups["frontend"])
329 assert.ElementsMatch(t, []string{"api.example.com", "db.example.com"}, groups["backend"])
330}
331
332func TestService_GetAttachments(t *testing.T) {
333 v := viper.New()
334 v.Set("attachments", map[string]interface{}{
335 "app.example.com": []interface{}{"redis:latest", "postgres:15"},
336 })
337
338 eventBus := mocks.NewMockEventPublisher(t)
339 svc := NewService(v, eventBus)
340 ctx := testContext()
341
342 _ = svc.Load(ctx)
343
344 attachments := svc.GetAttachments()
345
346 assert.Len(t, attachments, 1)
347 assert.ElementsMatch(t, []string{"redis:latest", "postgres:15"}, attachments["app.example.com"])
348}
349
350func TestService_GetExternalRoutes(t *testing.T) {
351 v := viper.New()
352 v.Set("external_routes", map[string]interface{}{
353 "reg.example.com": "localhost:5000",
354 "cache.example.com": "127.0.0.1:6379",
355 })
356
357 eventBus := mocks.NewMockEventPublisher(t)
358 svc := NewService(v, eventBus)
359 ctx := testContext()
360
361 _ = svc.Load(ctx)
362
363 routes := svc.GetExternalRoutes()
364
365 assert.Len(t, routes, 2)
366 assert.Equal(t, "localhost:5000", routes["reg.example.com"])
367 assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
368}
369
370func TestService_GetExternalRoutes_Empty(t *testing.T) {
371 v := viper.New()
372
373 eventBus := mocks.NewMockEventPublisher(t)
374 svc := NewService(v, eventBus)
375 ctx := testContext()
376
377 _ = svc.Load(ctx)
378
379 routes := svc.GetExternalRoutes()
380
381 assert.Empty(t, routes)
382}
383
384func TestExtractDomainFromImageName(t *testing.T) {
385 tests := []struct {
386 name string
387 imageName string
388 expectedDom string
389 expectedFound bool
390 }{
391 {
392 name: "domain like image",
393 imageName: "myapp.example.com:latest",
394 expectedDom: "myapp.example.com",
395 expectedFound: true,
396 },
397 {
398 name: "domain like image without tag",
399 imageName: "api.backend.io",
400 expectedDom: "api.backend.io",
401 expectedFound: true,
402 },
403 {
404 name: "simple image name",
405 imageName: "nginx:latest",
406 expectedDom: "",
407 expectedFound: false,
408 },
409 {
410 name: "registry path",
411 imageName: "gcr.io/project/image:tag",
412 expectedDom: "gcr.io/project/image",
413 expectedFound: true,
414 },
415 {
416 name: "leading dot",
417 imageName: ".invalid:latest",
418 expectedDom: "",
419 expectedFound: false,
420 },
421 {
422 name: "trailing dot",
423 imageName: "invalid.:latest",
424 expectedDom: "",
425 expectedFound: false,
426 },
427 }
428
429 for _, tt := range tests {
430 t.Run(tt.name, func(t *testing.T) {
431 domain, found := ExtractDomainFromImageName(tt.imageName)
432 assert.Equal(t, tt.expectedDom, domain)
433 assert.Equal(t, tt.expectedFound, found)
434 })
435 }
436}
437
438func TestLoadStringMap(t *testing.T) {
439 tests := []struct {
440 name string
441 input any
442 expected map[string]string
443 }{
444 {
445 name: "nil input",
446 input: nil,
447 expected: map[string]string{},
448 },
449 {
450 name: "valid map",
451 input: map[string]any{
452 "key1": "value1",
453 "key2": "value2",
454 },
455 expected: map[string]string{
456 "key1": "value1",
457 "key2": "value2",
458 },
459 },
460 {
461 name: "invalid type",
462 input: "not a map",
463 expected: map[string]string{},
464 },
465 {
466 name: "mixed values",
467 input: map[string]any{
468 "string": "value",
469 "int": 123,
470 },
471 expected: map[string]string{
472 "string": "value",
473 },
474 },
475 }
476
477 for _, tt := range tests {
478 t.Run(tt.name, func(t *testing.T) {
479 result := loadStringMap(tt.input)
480 assert.Equal(t, tt.expected, result)
481 })
482 }
483}
484
485func TestLoadStringArrayMap(t *testing.T) {
486 tests := []struct {
487 name string
488 input any
489 expected map[string][]string
490 }{
491 {
492 name: "nil input",
493 input: nil,
494 expected: map[string][]string{},
495 },
496 {
497 name: "valid map",
498 input: map[string]any{
499 "group1": []any{"a", "b"},
500 "group2": []any{"c", "d", "e"},
501 },
502 expected: map[string][]string{
503 "group1": {"a", "b"},
504 "group2": {"c", "d", "e"},
505 },
506 },
507 {
508 name: "invalid type",
509 input: "not a map",
510 expected: map[string][]string{},
511 },
512 {
513 name: "non-array value",
514 input: map[string]any{
515 "array": []any{"a", "b"},
516 "string": "not an array",
517 },
518 expected: map[string][]string{
519 "array": {"a", "b"},
520 },
521 },
522 }
523
524 for _, tt := range tests {
525 t.Run(tt.name, func(t *testing.T) {
526 result := loadStringArrayMap(tt.input)
527 assert.Equal(t, tt.expected, result)
528 })
529 }
530}