1package config
2
3import (
4 "context"
5 "testing"
6
7 "github.com/bnema/zerowrap"
8 "github.com/spf13/viper"
9 "github.com/stretchr/testify/assert"
10
11 "gordon/internal/boundaries/out/mocks"
12 "gordon/internal/domain"
13)
14
15func testContext() context.Context {
16 return zerowrap.WithCtx(context.Background(), zerowrap.Default())
17}
18
19func TestService_Load(t *testing.T) {
20 v := viper.New()
21 v.Set("server.port", 8080)
22 v.Set("server.registry_port", 5000)
23 v.Set("server.registry_domain", "registry.example.com")
24 v.Set("server.data_dir", "/var/gordon")
25 v.Set("auto_route.enabled", true)
26 v.Set("network_isolation.enabled", true)
27 v.Set("network_isolation.network_prefix", "gordon")
28 v.Set("registry_auth.enabled", true)
29 v.Set("registry_auth.username", "admin")
30 v.Set("registry_auth.password", "secret")
31 v.Set("volumes.auto_create", true)
32 v.Set("volumes.prefix", "gordon")
33 v.Set("volumes.preserve", false)
34 v.Set("routes", map[string]interface{}{
35 "app.example.com": "myapp:latest",
36 })
37
38 eventBus := mocks.NewMockEventPublisher(t)
39 svc := NewService(v, eventBus)
40 ctx := testContext()
41
42 err := svc.Load(ctx)
43
44 assert.NoError(t, err)
45 assert.Equal(t, 8080, svc.GetServerPort())
46 assert.Equal(t, 5000, svc.GetRegistryPort())
47 assert.Equal(t, "registry.example.com", svc.GetRegistryDomain())
48 assert.Equal(t, "/var/gordon", svc.GetDataDir())
49 assert.True(t, svc.IsAutoRouteEnabled())
50 assert.True(t, svc.IsNetworkIsolationEnabled())
51 assert.Equal(t, "gordon", svc.GetNetworkPrefix())
52
53 enabled, username, password := svc.GetRegistryAuthConfig()
54 assert.True(t, enabled)
55 assert.Equal(t, "admin", username)
56 assert.Equal(t, "secret", password)
57
58 autoCreate, prefix, preserve := svc.GetVolumeConfig()
59 assert.True(t, autoCreate)
60 assert.Equal(t, "gordon", prefix)
61 assert.False(t, preserve)
62}
63
64func TestService_GetRoutes(t *testing.T) {
65 v := viper.New()
66 v.Set("routes", map[string]interface{}{
67 "app1.example.com": "myapp:latest",
68 "app2.example.com": "otherapp:v2",
69 "http://insecure.example": "insecureapp:latest",
70 })
71
72 eventBus := mocks.NewMockEventPublisher(t)
73 svc := NewService(v, eventBus)
74 ctx := testContext()
75
76 _ = svc.Load(ctx)
77
78 routes := svc.GetRoutes(ctx)
79
80 assert.Len(t, routes, 3)
81
82 // Check routes (order is not guaranteed)
83 routeMap := make(map[string]domain.Route)
84 for _, r := range routes {
85 routeMap[r.Domain] = r
86 }
87
88 assert.Equal(t, "myapp:latest", routeMap["app1.example.com"].Image)
89 assert.True(t, routeMap["app1.example.com"].HTTPS)
90
91 assert.Equal(t, "otherapp:v2", routeMap["app2.example.com"].Image)
92 assert.True(t, routeMap["app2.example.com"].HTTPS)
93
94 assert.Equal(t, "insecureapp:latest", routeMap["insecure.example"].Image)
95 assert.False(t, routeMap["insecure.example"].HTTPS)
96}
97
98func TestService_AddRoute(t *testing.T) {
99 v := viper.New()
100 eventBus := mocks.NewMockEventPublisher(t)
101 svc := NewService(v, eventBus)
102 ctx := testContext()
103
104 _ = svc.Load(ctx)
105
106 route := domain.Route{
107 Domain: "new.example.com",
108 Image: "newapp:latest",
109 }
110
111 err := svc.AddRoute(ctx, route)
112
113 assert.NoError(t, err)
114
115 // Verify route was added
116 config := svc.GetConfig()
117 assert.Equal(t, "newapp:latest", config.Routes["new.example.com"])
118}
119
120func TestService_UpdateRoute(t *testing.T) {
121 v := viper.New()
122 v.Set("routes", map[string]interface{}{
123 "app.example.com": "myapp:v1",
124 })
125
126 eventBus := mocks.NewMockEventPublisher(t)
127 svc := NewService(v, eventBus)
128 ctx := testContext()
129
130 _ = svc.Load(ctx)
131
132 route := domain.Route{
133 Domain: "app.example.com",
134 Image: "myapp:v2",
135 }
136
137 err := svc.UpdateRoute(ctx, route)
138
139 assert.NoError(t, err)
140
141 config := svc.GetConfig()
142 assert.Equal(t, "myapp:v2", config.Routes["app.example.com"])
143}
144
145func TestService_UpdateRoute_NotFound(t *testing.T) {
146 v := viper.New()
147 eventBus := mocks.NewMockEventPublisher(t)
148 svc := NewService(v, eventBus)
149 ctx := testContext()
150
151 _ = svc.Load(ctx)
152
153 route := domain.Route{
154 Domain: "nonexistent.example.com",
155 Image: "myapp:latest",
156 }
157
158 err := svc.UpdateRoute(ctx, route)
159
160 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
161}
162
163func TestService_RemoveRoute(t *testing.T) {
164 v := viper.New()
165 v.Set("routes", map[string]interface{}{
166 "app.example.com": "myapp:latest",
167 })
168
169 eventBus := mocks.NewMockEventPublisher(t)
170 svc := NewService(v, eventBus)
171 ctx := testContext()
172
173 _ = svc.Load(ctx)
174
175 err := svc.RemoveRoute(ctx, "app.example.com")
176
177 assert.NoError(t, err)
178
179 config := svc.GetConfig()
180 _, exists := config.Routes["app.example.com"]
181 assert.False(t, exists)
182}
183
184func TestService_RemoveRoute_NotFound(t *testing.T) {
185 v := viper.New()
186 eventBus := mocks.NewMockEventPublisher(t)
187 svc := NewService(v, eventBus)
188 ctx := testContext()
189
190 _ = svc.Load(ctx)
191
192 err := svc.RemoveRoute(ctx, "nonexistent.example.com")
193
194 assert.ErrorIs(t, err, domain.ErrRouteNotFound)
195}
196
197func TestService_GetNetworkGroups(t *testing.T) {
198 v := viper.New()
199 v.Set("network_groups", map[string]interface{}{
200 "frontend": []interface{}{"app1.example.com", "app2.example.com"},
201 "backend": []interface{}{"api.example.com", "db.example.com"},
202 })
203
204 eventBus := mocks.NewMockEventPublisher(t)
205 svc := NewService(v, eventBus)
206 ctx := testContext()
207
208 _ = svc.Load(ctx)
209
210 groups := svc.GetNetworkGroups()
211
212 assert.Len(t, groups, 2)
213 assert.ElementsMatch(t, []string{"app1.example.com", "app2.example.com"}, groups["frontend"])
214 assert.ElementsMatch(t, []string{"api.example.com", "db.example.com"}, groups["backend"])
215}
216
217func TestService_GetAttachments(t *testing.T) {
218 v := viper.New()
219 v.Set("attachments", map[string]interface{}{
220 "app.example.com": []interface{}{"redis:latest", "postgres:15"},
221 })
222
223 eventBus := mocks.NewMockEventPublisher(t)
224 svc := NewService(v, eventBus)
225 ctx := testContext()
226
227 _ = svc.Load(ctx)
228
229 attachments := svc.GetAttachments()
230
231 assert.Len(t, attachments, 1)
232 assert.ElementsMatch(t, []string{"redis:latest", "postgres:15"}, attachments["app.example.com"])
233}
234
235func TestService_GetExternalRoutes(t *testing.T) {
236 v := viper.New()
237 v.Set("external_routes", map[string]interface{}{
238 "reg.example.com": "localhost:5000",
239 "cache.example.com": "127.0.0.1:6379",
240 })
241
242 eventBus := mocks.NewMockEventPublisher(t)
243 svc := NewService(v, eventBus)
244 ctx := testContext()
245
246 _ = svc.Load(ctx)
247
248 routes := svc.GetExternalRoutes()
249
250 assert.Len(t, routes, 2)
251 assert.Equal(t, "localhost:5000", routes["reg.example.com"])
252 assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
253}
254
255func TestService_GetExternalRoutes_Empty(t *testing.T) {
256 v := viper.New()
257
258 eventBus := mocks.NewMockEventPublisher(t)
259 svc := NewService(v, eventBus)
260 ctx := testContext()
261
262 _ = svc.Load(ctx)
263
264 routes := svc.GetExternalRoutes()
265
266 assert.Empty(t, routes)
267}
268
269func TestExtractDomainFromImageName(t *testing.T) {
270 tests := []struct {
271 name string
272 imageName string
273 expectedDom string
274 expectedFound bool
275 }{
276 {
277 name: "domain like image",
278 imageName: "myapp.example.com:latest",
279 expectedDom: "myapp.example.com",
280 expectedFound: true,
281 },
282 {
283 name: "domain like image without tag",
284 imageName: "api.backend.io",
285 expectedDom: "api.backend.io",
286 expectedFound: true,
287 },
288 {
289 name: "simple image name",
290 imageName: "nginx:latest",
291 expectedDom: "",
292 expectedFound: false,
293 },
294 {
295 name: "registry path",
296 imageName: "gcr.io/project/image:tag",
297 expectedDom: "gcr.io/project/image",
298 expectedFound: true,
299 },
300 {
301 name: "leading dot",
302 imageName: ".invalid:latest",
303 expectedDom: "",
304 expectedFound: false,
305 },
306 {
307 name: "trailing dot",
308 imageName: "invalid.:latest",
309 expectedDom: "",
310 expectedFound: false,
311 },
312 }
313
314 for _, tt := range tests {
315 t.Run(tt.name, func(t *testing.T) {
316 domain, found := ExtractDomainFromImageName(tt.imageName)
317 assert.Equal(t, tt.expectedDom, domain)
318 assert.Equal(t, tt.expectedFound, found)
319 })
320 }
321}
322
323func TestLoadStringMap(t *testing.T) {
324 tests := []struct {
325 name string
326 input any
327 expected map[string]string
328 }{
329 {
330 name: "nil input",
331 input: nil,
332 expected: map[string]string{},
333 },
334 {
335 name: "valid map",
336 input: map[string]any{
337 "key1": "value1",
338 "key2": "value2",
339 },
340 expected: map[string]string{
341 "key1": "value1",
342 "key2": "value2",
343 },
344 },
345 {
346 name: "invalid type",
347 input: "not a map",
348 expected: map[string]string{},
349 },
350 {
351 name: "mixed values",
352 input: map[string]any{
353 "string": "value",
354 "int": 123,
355 },
356 expected: map[string]string{
357 "string": "value",
358 },
359 },
360 }
361
362 for _, tt := range tests {
363 t.Run(tt.name, func(t *testing.T) {
364 result := loadStringMap(tt.input)
365 assert.Equal(t, tt.expected, result)
366 })
367 }
368}
369
370func TestLoadStringArrayMap(t *testing.T) {
371 tests := []struct {
372 name string
373 input any
374 expected map[string][]string
375 }{
376 {
377 name: "nil input",
378 input: nil,
379 expected: map[string][]string{},
380 },
381 {
382 name: "valid map",
383 input: map[string]any{
384 "group1": []any{"a", "b"},
385 "group2": []any{"c", "d", "e"},
386 },
387 expected: map[string][]string{
388 "group1": {"a", "b"},
389 "group2": {"c", "d", "e"},
390 },
391 },
392 {
393 name: "invalid type",
394 input: "not a map",
395 expected: map[string][]string{},
396 },
397 {
398 name: "non-array value",
399 input: map[string]any{
400 "array": []any{"a", "b"},
401 "string": "not an array",
402 },
403 expected: map[string][]string{
404 "array": {"a", "b"},
405 },
406 },
407 }
408
409 for _, tt := range tests {
410 t.Run(tt.name, func(t *testing.T) {
411 result := loadStringArrayMap(tt.input)
412 assert.Equal(t, tt.expected, result)
413 })
414 }
415}