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 TestExtractDomainFromImageName(t *testing.T) {
236 tests := []struct {
237 name string
238 imageName string
239 expectedDom string
240 expectedFound bool
241 }{
242 {
243 name: "domain like image",
244 imageName: "myapp.example.com:latest",
245 expectedDom: "myapp.example.com",
246 expectedFound: true,
247 },
248 {
249 name: "domain like image without tag",
250 imageName: "api.backend.io",
251 expectedDom: "api.backend.io",
252 expectedFound: true,
253 },
254 {
255 name: "simple image name",
256 imageName: "nginx:latest",
257 expectedDom: "",
258 expectedFound: false,
259 },
260 {
261 name: "registry path",
262 imageName: "gcr.io/project/image:tag",
263 expectedDom: "gcr.io/project/image",
264 expectedFound: true,
265 },
266 {
267 name: "leading dot",
268 imageName: ".invalid:latest",
269 expectedDom: "",
270 expectedFound: false,
271 },
272 {
273 name: "trailing dot",
274 imageName: "invalid.:latest",
275 expectedDom: "",
276 expectedFound: false,
277 },
278 }
279
280 for _, tt := range tests {
281 t.Run(tt.name, func(t *testing.T) {
282 domain, found := ExtractDomainFromImageName(tt.imageName)
283 assert.Equal(t, tt.expectedDom, domain)
284 assert.Equal(t, tt.expectedFound, found)
285 })
286 }
287}
288
289func TestLoadStringMap(t *testing.T) {
290 tests := []struct {
291 name string
292 input any
293 expected map[string]string
294 }{
295 {
296 name: "nil input",
297 input: nil,
298 expected: map[string]string{},
299 },
300 {
301 name: "valid map",
302 input: map[string]any{
303 "key1": "value1",
304 "key2": "value2",
305 },
306 expected: map[string]string{
307 "key1": "value1",
308 "key2": "value2",
309 },
310 },
311 {
312 name: "invalid type",
313 input: "not a map",
314 expected: map[string]string{},
315 },
316 {
317 name: "mixed values",
318 input: map[string]any{
319 "string": "value",
320 "int": 123,
321 },
322 expected: map[string]string{
323 "string": "value",
324 },
325 },
326 }
327
328 for _, tt := range tests {
329 t.Run(tt.name, func(t *testing.T) {
330 result := loadStringMap(tt.input)
331 assert.Equal(t, tt.expected, result)
332 })
333 }
334}
335
336func TestLoadStringArrayMap(t *testing.T) {
337 tests := []struct {
338 name string
339 input any
340 expected map[string][]string
341 }{
342 {
343 name: "nil input",
344 input: nil,
345 expected: map[string][]string{},
346 },
347 {
348 name: "valid map",
349 input: map[string]any{
350 "group1": []any{"a", "b"},
351 "group2": []any{"c", "d", "e"},
352 },
353 expected: map[string][]string{
354 "group1": {"a", "b"},
355 "group2": {"c", "d", "e"},
356 },
357 },
358 {
359 name: "invalid type",
360 input: "not a map",
361 expected: map[string][]string{},
362 },
363 {
364 name: "non-array value",
365 input: map[string]any{
366 "array": []any{"a", "b"},
367 "string": "not an array",
368 },
369 expected: map[string][]string{
370 "array": {"a", "b"},
371 },
372 },
373 }
374
375 for _, tt := range tests {
376 t.Run(tt.name, func(t *testing.T) {
377 result := loadStringArrayMap(tt.input)
378 assert.Equal(t, tt.expected, result)
379 })
380 }
381}