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)
16
17func testContext() context.Context {
18 return zerowrap.WithCtx(context.Background(), zerowrap.Default())
19}
20
21func TestService_Load(t *testing.T) {
22 v := viper.New()
23 v.Set("server.port", 8080)
24 v.Set("server.registry_port", 5000)
25 v.Set("server.registry_domain", "registry.example.com")
26 v.Set("server.data_dir", "/var/gordon")
27 v.Set("auto_route.enabled", true)
28 v.Set("network_isolation.enabled", true)
29 v.Set("network_isolation.network_prefix", "gordon")
30 v.Set("auth.enabled", true)
31 v.Set("auth.username", "admin")
32 v.Set("auth.password", "secret")
33 v.Set("volumes.auto_create", true)
34 v.Set("volumes.prefix", "gordon")
35 v.Set("volumes.preserve", false)
36
37 eventBus := mocks.NewMockEventPublisher(t)
38 svc := NewService(v, eventBus)
39 ctx := testContext()
40
41 err := svc.Load(ctx)
42
43 assert.NoError(t, err)
44 assert.Equal(t, 8080, svc.GetServerPort())
45 assert.Equal(t, 5000, svc.GetRegistryPort())
46 assert.Equal(t, "registry.example.com", svc.GetRegistryDomain())
47 assert.Equal(t, "/var/gordon", svc.GetDataDir())
48 assert.True(t, svc.IsNetworkIsolationEnabled())
49 assert.Equal(t, "gordon", svc.GetNetworkPrefix())
50
51 autoCreate, prefix, preserve := svc.GetVolumeConfig()
52 assert.True(t, autoCreate)
53 assert.Equal(t, "gordon", prefix)
54 assert.False(t, preserve)
55}
56
57func TestService_LoadPrefersExplicitRegistryAddress(t *testing.T) {
58 v := viper.New()
59 v.Set("server.gordon_domain", "gordon.example.com")
60 v.Set("server.registry_domain", "100.64.0.10:15000")
61
62 svc := NewService(v, mocks.NewMockEventPublisher(t))
63 require.NoError(t, svc.Load(testContext()))
64 assert.Equal(t, "100.64.0.10:15000", svc.GetRegistryDomain())
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 err := os.WriteFile(configFile, []byte(initialConfig), 0600)
76 require.NoError(t, err)
77
78 v := viper.New()
79 v.SetConfigFile(configFile)
80 err = v.ReadInConfig()
81 require.NoError(t, err)
82
83 eventBus := mocks.NewMockEventPublisher(t)
84 svc := NewService(v, eventBus)
85 ctx := testContext()
86
87 // Initial load
88 err = svc.Load(ctx)
89 require.NoError(t, err)
90 assert.Equal(t, 8080, svc.GetServerPort())
91
92 // Modify config file on disk
93 updatedConfig := `[server]
94port = 9090
95`
96 err = os.WriteFile(configFile, []byte(updatedConfig), 0600)
97 require.NoError(t, err)
98
99 // Reload should pick up new values
100 err = svc.Reload(ctx)
101 require.NoError(t, err)
102
103 assert.Equal(t, 9090, svc.GetServerPort())
104 })
105
106 t.Run("error - config file not found", func(t *testing.T) {
107 tmpDir := t.TempDir()
108 configFile := filepath.Join(tmpDir, "gordon.toml")
109
110 // Create config, load it, then delete it
111 err := os.WriteFile(configFile, []byte("[server]\nport = 8080\n"), 0600)
112 require.NoError(t, err)
113
114 v := viper.New()
115 v.SetConfigFile(configFile)
116 err = v.ReadInConfig()
117 require.NoError(t, err)
118
119 eventBus := mocks.NewMockEventPublisher(t)
120 svc := NewService(v, eventBus)
121 ctx := testContext()
122
123 err = svc.Load(ctx)
124 require.NoError(t, err)
125
126 // Delete the config file
127 err = os.Remove(configFile)
128 require.NoError(t, err)
129
130 // Reload should fail
131 err = svc.Reload(ctx)
132 assert.Error(t, err)
133 assert.Contains(t, err.Error(), "failed to read config file")
134 })
135
136 t.Run("error - invalid config syntax", func(t *testing.T) {
137 tmpDir := t.TempDir()
138 configFile := filepath.Join(tmpDir, "gordon.toml")
139
140 // Create valid config initially
141 err := os.WriteFile(configFile, []byte("[server]\nport = 8080\n"), 0600)
142 require.NoError(t, err)
143
144 v := viper.New()
145 v.SetConfigFile(configFile)
146 err = v.ReadInConfig()
147 require.NoError(t, err)
148
149 eventBus := mocks.NewMockEventPublisher(t)
150 svc := NewService(v, eventBus)
151 ctx := testContext()
152
153 err = svc.Load(ctx)
154 require.NoError(t, err)
155
156 // Write invalid TOML
157 err = os.WriteFile(configFile, []byte("[server\nport = invalid syntax"), 0600)
158 require.NoError(t, err)
159
160 // Reload should fail
161 err = svc.Reload(ctx)
162 assert.Error(t, err)
163 })
164}
165
166func TestLoadExternalRoutes_RejectsInvalidAndDuplicateKeys(t *testing.T) {
167 t.Run("invalid", func(t *testing.T) {
168 _, err := loadExternalRoutes(map[string]any{
169 "bad.example.com:8080": "203.0.113.10:5000",
170 })
171
172 require.Error(t, err)
173 assert.Contains(t, err.Error(), "invalid external route key")
174 })
175
176 t.Run("duplicate canonical", func(t *testing.T) {
177 _, err := loadExternalRoutes(map[string]any{
178 "Reg.Example.com": "203.0.113.10:5000",
179 "reg.example.com": "203.0.113.11:5000",
180 })
181
182 require.Error(t, err)
183 assert.Contains(t, err.Error(), "duplicate external route key")
184 })
185}
186
187func TestService_GetExternalRoutes(t *testing.T) {
188 v := viper.New()
189 v.Set("external_routes", map[string]interface{}{
190 "Reg.Example.com": "localhost:5000",
191 "cache.example.com": "127.0.0.1:6379",
192 })
193
194 eventBus := mocks.NewMockEventPublisher(t)
195 svc := NewService(v, eventBus)
196 ctx := testContext()
197
198 _ = svc.Load(ctx)
199
200 routes := svc.GetExternalRoutes()
201
202 assert.Len(t, routes, 2)
203 assert.Equal(t, "localhost:5000", routes["reg.example.com"])
204 assert.Equal(t, "127.0.0.1:6379", routes["cache.example.com"])
205}
206
207func TestService_GetExternalRoutes_Empty(t *testing.T) {
208 v := viper.New()
209
210 eventBus := mocks.NewMockEventPublisher(t)
211 svc := NewService(v, eventBus)
212 ctx := testContext()
213
214 _ = svc.Load(ctx)
215
216 routes := svc.GetExternalRoutes()
217
218 assert.Empty(t, routes)
219}
220
221func TestExtractDomainFromImageName(t *testing.T) {
222 tests := []struct {
223 name string
224 imageName string
225 expectedDom string
226 expectedFound bool
227 }{
228 {
229 name: "domain like image",
230 imageName: "myapp.example.com:latest",
231 expectedDom: "myapp.example.com",
232 expectedFound: true,
233 },
234 {
235 name: "domain like image without tag",
236 imageName: "api.backend.io",
237 expectedDom: "api.backend.io",
238 expectedFound: true,
239 },
240 {
241 name: "simple image name",
242 imageName: "nginx:latest",
243 expectedDom: "",
244 expectedFound: false,
245 },
246 {
247 name: "registry path",
248 imageName: "gcr.io/project/image:tag",
249 expectedDom: "gcr.io/project/image",
250 expectedFound: true,
251 },
252 {
253 name: "leading dot",
254 imageName: ".invalid:latest",
255 expectedDom: "",
256 expectedFound: false,
257 },
258 {
259 name: "trailing dot",
260 imageName: "invalid.:latest",
261 expectedDom: "",
262 expectedFound: false,
263 },
264 }
265
266 for _, tt := range tests {
267 t.Run(tt.name, func(t *testing.T) {
268 domain, found := ExtractDomainFromImageName(tt.imageName)
269 assert.Equal(t, tt.expectedDom, domain)
270 assert.Equal(t, tt.expectedFound, found)
271 })
272 }
273}
274
275func TestLoadStringMap(t *testing.T) {
276 tests := []struct {
277 name string
278 input any
279 expected map[string]string
280 }{
281 {
282 name: "nil input",
283 input: nil,
284 expected: map[string]string{},
285 },
286 {
287 name: "valid map",
288 input: map[string]any{
289 "key1": "value1",
290 "key2": "value2",
291 },
292 expected: map[string]string{
293 "key1": "value1",
294 "key2": "value2",
295 },
296 },
297 {
298 name: "invalid type",
299 input: "not a map",
300 expected: map[string]string{},
301 },
302 {
303 name: "mixed values",
304 input: map[string]any{
305 "string": "value",
306 "int": 123,
307 },
308 expected: map[string]string{
309 "string": "value",
310 },
311 },
312 }
313
314 for _, tt := range tests {
315 t.Run(tt.name, func(t *testing.T) {
316 result := loadStringMap(tt.input)
317 assert.Equal(t, tt.expected, result)
318 })
319 }
320}