1package config
2
3import (
4 "os"
5 "path/filepath"
6 "testing"
7
8 "github.com/spf13/viper"
9 "github.com/stretchr/testify/assert"
10 "github.com/stretchr/testify/require"
11
12 "gordon/internal/testutils"
13)
14
15func TestConfig_Load_ValidBasicConfig(t *testing.T) {
16 // Create temporary config file
17 configContent := testutils.LoadFixtureConfig(t, "basic.toml")
18
19 // Create a temporary directory for the config file
20 tempDir := t.TempDir()
21 configFile := filepath.Join(tempDir, "gordon.toml")
22
23 // Write config to temporary file
24 err := os.WriteFile(configFile, []byte(configContent), 0644)
25 require.NoError(t, err)
26
27 // Set viper to use this config file
28 viper.Reset()
29 viper.SetConfigFile(configFile)
30 err = viper.ReadInConfig()
31 require.NoError(t, err)
32
33 // Load config
34 cfg, err := Load()
35 require.NoError(t, err)
36 require.NotNil(t, cfg)
37
38 // Verify server config
39 assert.Equal(t, 8080, cfg.Server.Port)
40 assert.Equal(t, 5000, cfg.Server.RegistryPort)
41 assert.Equal(t, "registry.example.com", cfg.Server.RegistryDomain)
42 assert.Equal(t, "docker", cfg.Server.Runtime)
43
44 // Verify registry auth config
45 assert.True(t, cfg.RegistryAuth.Enabled)
46 assert.Equal(t, "admin", cfg.RegistryAuth.Username)
47 assert.Equal(t, "password123", cfg.RegistryAuth.Password)
48
49 // Verify routes
50 assert.Len(t, cfg.Routes, 2)
51 assert.Equal(t, "nginx:latest", cfg.Routes["app.example.com"])
52 assert.Equal(t, "myapi:v1", cfg.Routes["api.example.com"])
53
54 // Verify volumes config
55 assert.True(t, cfg.Volumes.AutoCreate)
56 assert.Equal(t, "gordon", cfg.Volumes.Prefix)
57 assert.True(t, cfg.Volumes.Preserve)
58
59 // Verify env config
60 assert.Equal(t, "/tmp/env", cfg.Env.Dir)
61 assert.Contains(t, cfg.Env.Providers, "pass")
62 assert.Contains(t, cfg.Env.Providers, "sops")
63
64 // Verify logging config
65 assert.True(t, cfg.Logging.Enabled)
66 assert.Equal(t, "info", cfg.Logging.Level)
67}
68
69func TestConfig_Load_MinimalConfig(t *testing.T) {
70 configContent := testutils.LoadFixtureConfig(t, "minimal.toml")
71
72 tempDir := t.TempDir()
73 configFile := filepath.Join(tempDir, "gordon.toml")
74
75 err := os.WriteFile(configFile, []byte(configContent), 0644)
76 require.NoError(t, err)
77
78 viper.Reset()
79 viper.SetConfigFile(configFile)
80 err = viper.ReadInConfig()
81 require.NoError(t, err)
82
83 cfg, err := Load()
84 require.NoError(t, err)
85 require.NotNil(t, cfg)
86
87 // Should have defaults applied where not specified, but explicit values where set
88 // Note: viper defaults may not apply if config structure is partially loaded
89 assert.Equal(t, "registry.example.com", cfg.Server.RegistryDomain)
90 assert.Equal(t, "docker", cfg.Server.Runtime) // explicitly set in minimal config
91 assert.False(t, cfg.RegistryAuth.Enabled) // explicitly set in minimal config
92 assert.True(t, cfg.Volumes.AutoCreate) // default
93 assert.Equal(t, "gordon", cfg.Volumes.Prefix) // default
94}
95
96func TestConfig_Load_InvalidTOMLSyntax(t *testing.T) {
97 configContent := testutils.LoadFixtureConfig(t, "invalid.toml")
98
99 tempDir := t.TempDir()
100 configFile := filepath.Join(tempDir, "gordon.toml")
101
102 err := os.WriteFile(configFile, []byte(configContent), 0644)
103 require.NoError(t, err)
104
105 viper.Reset()
106 viper.SetConfigFile(configFile)
107 err = viper.ReadInConfig()
108 require.Error(t, err)
109 assert.Contains(t, err.Error(), "toml")
110}
111
112func TestConfig_Load_InvalidRuntime(t *testing.T) {
113 configContent := `[server]
114registry_domain = "registry.example.com"
115runtime = "invalid-runtime"`
116
117 tempDir := t.TempDir()
118 configFile := filepath.Join(tempDir, "gordon.toml")
119
120 err := os.WriteFile(configFile, []byte(configContent), 0644)
121 require.NoError(t, err)
122
123 viper.Reset()
124 viper.SetConfigFile(configFile)
125 err = viper.ReadInConfig()
126 require.NoError(t, err)
127
128 _, err = Load()
129 require.Error(t, err)
130 assert.Contains(t, err.Error(), "server.runtime must be one of")
131}
132
133func TestConfig_Load_InvalidRegistryDomain(t *testing.T) {
134 testCases := []struct {
135 name string
136 domain string
137 }{
138 {"with protocol", "https://registry.example.com"},
139 {"with path", "registry.example.com/path"},
140 {"with protocol and path", "https://registry.example.com/path"},
141 }
142
143 for _, tc := range testCases {
144 t.Run(tc.name, func(t *testing.T) {
145 configContent := `[server]
146registry_domain = "` + tc.domain + `"
147runtime = "docker"
148
149[registry_auth]
150enabled = false`
151
152 tempDir := t.TempDir()
153 configFile := filepath.Join(tempDir, "gordon.toml")
154
155 err := os.WriteFile(configFile, []byte(configContent), 0644)
156 require.NoError(t, err)
157
158 viper.Reset()
159 viper.SetConfigFile(configFile)
160 err = viper.ReadInConfig()
161 require.NoError(t, err)
162
163 _, err = Load()
164 require.Error(t, err)
165 assert.Contains(t, err.Error(), "registry_domain should be just the domain name")
166 })
167 }
168}
169
170func TestConfig_Load_RegistryAuthValidation(t *testing.T) {
171 configContent := `[server]
172registry_domain = "registry.example.com"
173runtime = "docker"
174
175[registry_auth]
176enabled = true
177username = ""
178password = ""`
179
180 tempDir := t.TempDir()
181 configFile := filepath.Join(tempDir, "gordon.toml")
182
183 err := os.WriteFile(configFile, []byte(configContent), 0644)
184 require.NoError(t, err)
185
186 viper.Reset()
187 viper.SetConfigFile(configFile)
188 err = viper.ReadInConfig()
189 require.NoError(t, err)
190
191 _, err = Load()
192 require.Error(t, err)
193 assert.Contains(t, err.Error(), "Registry auth enabled but username/password not provided")
194}
195
196func TestConfig_GetRoutes(t *testing.T) {
197 cfg := &Config{
198 Routes: map[string]string{
199 "app.example.com": "nginx:latest",
200 "http://api.example.com": "api:v1",
201 "secure.example.com": "secure:latest",
202 },
203 }
204
205 routes := cfg.GetRoutes()
206
207 require.Len(t, routes, 3)
208
209 // Find routes by domain
210 var appRoute, apiRoute, secureRoute Route
211 for _, route := range routes {
212 switch route.Domain {
213 case "app.example.com":
214 appRoute = route
215 case "api.example.com":
216 apiRoute = route
217 case "secure.example.com":
218 secureRoute = route
219 }
220 }
221
222 // Verify HTTPS defaults
223 assert.Equal(t, "nginx:latest", appRoute.Image)
224 assert.True(t, appRoute.HTTPS)
225
226 // Verify HTTP override
227 assert.Equal(t, "api:v1", apiRoute.Image)
228 assert.False(t, apiRoute.HTTPS)
229
230 // Verify HTTPS default
231 assert.Equal(t, "secure:latest", secureRoute.Image)
232 assert.True(t, secureRoute.HTTPS)
233}
234
235func TestExtractDomainFromImageName(t *testing.T) {
236 testCases := []struct {
237 name string
238 imageName string
239 expectedDomain string
240 shouldMatch bool
241 }{
242 {"valid domain with tag", "app.example.com:latest", "app.example.com", true},
243 {"valid domain without tag", "api.example.com", "api.example.com", true},
244 {"subdomain", "staging.api.example.com:v1", "staging.api.example.com", true},
245 {"simple name", "nginx", "", false},
246 {"dockerhub format", "user/repo", "", false},
247 {"localhost", "localhost:5000", "", false},
248 {"ip address", "192.168.1.1:8080", "", false},
249 {"invalid domain", "app-.example.com", "", false},
250 }
251
252 for _, tc := range testCases {
253 t.Run(tc.name, func(t *testing.T) {
254 domain, match := ExtractDomainFromImageName(tc.imageName)
255 assert.Equal(t, tc.shouldMatch, match)
256 assert.Equal(t, tc.expectedDomain, domain)
257 })
258 }
259}
260
261func TestConfig_AddRoute(t *testing.T) {
262 // Create a config with existing routes
263 configContent := `[server]
264registry_domain = "registry.example.com"
265runtime = "docker"
266
267[registry_auth]
268enabled = false
269
270[routes]
271"existing.example.com" = "existing:latest"`
272
273 tempDir := t.TempDir()
274 configFile := filepath.Join(tempDir, "gordon.toml")
275
276 err := os.WriteFile(configFile, []byte(configContent), 0644)
277 require.NoError(t, err)
278
279 viper.Reset()
280 viper.SetConfigFile(configFile)
281 err = viper.ReadInConfig()
282 require.NoError(t, err)
283
284 cfg, err := Load()
285 require.NoError(t, err)
286
287 // Add a new route
288 err = cfg.AddRoute("new.example.com", "newapp:v1")
289 require.NoError(t, err)
290
291 // Verify route was added to memory
292 assert.Equal(t, "newapp:v1", cfg.Routes["new.example.com"])
293
294 // Verify config file was updated
295 updatedContent, err := os.ReadFile(configFile)
296 require.NoError(t, err)
297 assert.Contains(t, string(updatedContent), `"new.example.com" = "newapp:v1"`)
298 assert.Contains(t, string(updatedContent), `"existing.example.com" = "existing:latest"`)
299}
300
301func TestConfig_UpdateRoute(t *testing.T) {
302 configContent := `[server]
303registry_domain = "registry.example.com"
304runtime = "docker"
305
306[registry_auth]
307enabled = false
308
309[routes]
310"app.example.com" = "oldapp:v1"`
311
312 tempDir := t.TempDir()
313 configFile := filepath.Join(tempDir, "gordon.toml")
314
315 err := os.WriteFile(configFile, []byte(configContent), 0644)
316 require.NoError(t, err)
317
318 viper.Reset()
319 viper.SetConfigFile(configFile)
320 err = viper.ReadInConfig()
321 require.NoError(t, err)
322
323 cfg, err := Load()
324 require.NoError(t, err)
325
326 // Update existing route
327 err = cfg.UpdateRoute("app.example.com", "newapp:v2")
328 require.NoError(t, err)
329
330 // Verify route was updated in memory
331 assert.Equal(t, "newapp:v2", cfg.Routes["app.example.com"])
332
333 // Verify config file was updated
334 updatedContent, err := os.ReadFile(configFile)
335 require.NoError(t, err)
336 assert.Contains(t, string(updatedContent), `"app.example.com" = "newapp:v2"`)
337 assert.NotContains(t, string(updatedContent), "oldapp:v1")
338}
339
340func TestConfig_UpdateRoute_NonExistent(t *testing.T) {
341 configContent := `[server]
342registry_domain = "registry.example.com"
343runtime = "docker"
344
345[registry_auth]
346enabled = false
347
348[routes]`
349
350 tempDir := t.TempDir()
351 configFile := filepath.Join(tempDir, "gordon.toml")
352
353 err := os.WriteFile(configFile, []byte(configContent), 0644)
354 require.NoError(t, err)
355
356 viper.Reset()
357 viper.SetConfigFile(configFile)
358 err = viper.ReadInConfig()
359 require.NoError(t, err)
360
361 cfg, err := Load()
362 require.NoError(t, err)
363
364 // Try to update non-existent route
365 err = cfg.UpdateRoute("nonexistent.example.com", "app:v1")
366 require.Error(t, err)
367 assert.Contains(t, err.Error(), "route nonexistent.example.com does not exist")
368}
369
370func TestValidateEnvConfig(t *testing.T) {
371 testCases := []struct {
372 name string
373 envConfig EnvConfig
374 expectError bool
375 errorText string
376 }{
377 {
378 name: "valid config",
379 envConfig: EnvConfig{
380 Dir: "/tmp/env",
381 Providers: []string{"pass", "sops"},
382 },
383 expectError: false,
384 },
385 {
386 name: "empty provider",
387 envConfig: EnvConfig{
388 Dir: "/tmp/env",
389 Providers: []string{"pass", "", "sops"},
390 },
391 expectError: true,
392 errorText: "empty provider name",
393 },
394 {
395 name: "invalid provider",
396 envConfig: EnvConfig{
397 Dir: "/tmp/env",
398 Providers: []string{"pass", "invalid", "sops"},
399 },
400 expectError: true,
401 errorText: "unsupported provider 'invalid'",
402 },
403 {
404 name: "non-existent directory file conflict",
405 envConfig: EnvConfig{
406 Dir: "/dev/null", // This exists but is not a directory
407 Providers: []string{"pass"},
408 },
409 expectError: true,
410 errorText: "not a directory",
411 },
412 }
413
414 for _, tc := range testCases {
415 t.Run(tc.name, func(t *testing.T) {
416 err := validateEnvConfig(&tc.envConfig)
417 if tc.expectError {
418 require.Error(t, err)
419 assert.Contains(t, err.Error(), tc.errorText)
420 } else {
421 require.NoError(t, err)
422 }
423 })
424 }
425}
426
427func TestGetDefaultDataDir(t *testing.T) {
428 // Test should not fail regardless of environment
429 dataDir := getDefaultDataDir()
430 assert.NotEmpty(t, dataDir)
431
432 // Should be either absolute path (for user home) or relative "./data"
433 assert.True(t, filepath.IsAbs(dataDir) || dataDir == "./data")
434}
435
436func TestConfig_Load_NetworkIsolation(t *testing.T) {
437 configContent := `[server]
438registry_domain = "registry.example.com"
439runtime = "docker"
440
441[registry_auth]
442enabled = false
443
444[network_isolation]
445enabled = true
446network_prefix = "custom"
447dns_suffix = ".local"`
448
449 tempDir := t.TempDir()
450 configFile := filepath.Join(tempDir, "gordon.toml")
451
452 err := os.WriteFile(configFile, []byte(configContent), 0644)
453 require.NoError(t, err)
454
455 viper.Reset()
456 viper.SetConfigFile(configFile)
457 err = viper.ReadInConfig()
458 require.NoError(t, err)
459
460 cfg, err := Load()
461 require.NoError(t, err)
462
463 assert.True(t, cfg.NetworkIsolation.Enabled)
464 assert.Equal(t, "custom", cfg.NetworkIsolation.NetworkPrefix)
465 assert.Equal(t, ".local", cfg.NetworkIsolation.DNSSuffix)
466}
467
468func TestConfig_Load_AttachmentsAndNetworkGroups(t *testing.T) {
469 configContent := `[server]
470registry_domain = "registry.example.com"
471runtime = "docker"
472
473[registry_auth]
474enabled = false
475
476[attachments]
477"app.example.com" = ["postgres:14", "redis:6"]
478"backend" = ["postgres:14"]
479
480[network_groups]
481"backend" = ["api.example.com", "worker.example.com"]`
482
483 tempDir := t.TempDir()
484 configFile := filepath.Join(tempDir, "gordon.toml")
485
486 err := os.WriteFile(configFile, []byte(configContent), 0644)
487 require.NoError(t, err)
488
489 viper.Reset()
490 viper.SetConfigFile(configFile)
491 err = viper.ReadInConfig()
492 require.NoError(t, err)
493
494 cfg, err := Load()
495 require.NoError(t, err)
496
497 // Verify attachments
498 assert.Len(t, cfg.Attachments, 2)
499 assert.Equal(t, []string{"postgres:14", "redis:6"}, cfg.Attachments["app.example.com"])
500 assert.Equal(t, []string{"postgres:14"}, cfg.Attachments["backend"])
501
502 // Verify network groups
503 assert.Len(t, cfg.NetworkGroups, 1)
504 assert.Equal(t, []string{"api.example.com", "worker.example.com"}, cfg.NetworkGroups["backend"])
505}
506
507func TestConfig_Load_EmptyConfiguration(t *testing.T) {
508 // Test loading with completely empty config file but needs minimal valid config
509 configContent := `[registry_auth]
510enabled = false`
511
512 tempDir := t.TempDir()
513 configFile := filepath.Join(tempDir, "gordon.toml")
514
515 err := os.WriteFile(configFile, []byte(configContent), 0644)
516 require.NoError(t, err)
517
518 viper.Reset()
519 viper.SetConfigFile(configFile)
520 err = viper.ReadInConfig()
521 require.NoError(t, err)
522
523 cfg, err := Load()
524 require.NoError(t, err)
525
526 // Should have all defaults except what we explicitly set
527 assert.Equal(t, 8080, cfg.Server.Port)
528 assert.Equal(t, 5000, cfg.Server.RegistryPort)
529 assert.Equal(t, "auto", cfg.Server.Runtime)
530 assert.False(t, cfg.RegistryAuth.Enabled) // explicitly disabled
531 assert.True(t, cfg.Volumes.AutoCreate)
532 assert.Equal(t, "gordon", cfg.Volumes.Prefix)
533 assert.True(t, cfg.Volumes.Preserve)
534 assert.True(t, cfg.NetworkIsolation.Enabled)
535 assert.Equal(t, "gordon", cfg.NetworkIsolation.NetworkPrefix)
536 assert.Equal(t, ".internal", cfg.NetworkIsolation.DNSSuffix)
537}
538
539func TestConfig_AddRoute_NoConfigFile(t *testing.T) {
540 // Clear viper state to ensure no config file is set
541 viper.Reset()
542
543 cfg := &Config{
544 Routes: make(map[string]string),
545 }
546
547 // Try to add route when no config file is available
548 err := cfg.AddRoute("test.example.com", "test:latest")
549 require.Error(t, err)
550 assert.Contains(t, err.Error(), "no config file path available")
551}
552
553func TestConfig_AddRoute_FileReadError(t *testing.T) {
554 // Create a config file in a non-existent directory
555 configFile := "/nonexistent/directory/gordon.toml"
556
557 viper.Reset()
558 viper.SetConfigFile(configFile)
559
560 cfg := &Config{
561 Routes: make(map[string]string),
562 }
563
564 err := cfg.AddRoute("test.example.com", "test:latest")
565 require.Error(t, err)
566 assert.Contains(t, err.Error(), "failed to read config file")
567}
568
569func TestConfig_UpdateRoute_FileReadError(t *testing.T) {
570 // Create a config file in a non-existent directory
571 configFile := "/nonexistent/directory/gordon.toml"
572
573 viper.Reset()
574 viper.SetConfigFile(configFile)
575
576 cfg := &Config{
577 Routes: map[string]string{
578 "test.example.com": "test:v1",
579 },
580 }
581
582 err := cfg.UpdateRoute("test.example.com", "test:v2")
583 require.Error(t, err)
584 assert.Contains(t, err.Error(), "failed to read config file")
585}
586
587func TestConfig_AddRoute_RouteAtEndOfFile(t *testing.T) {
588 configContent := `[server]
589registry_domain = "registry.example.com"
590runtime = "docker"
591
592[registry_auth]
593enabled = false
594
595[routes]
596"existing.example.com" = "existing:latest"`
597
598 tempDir := t.TempDir()
599 configFile := filepath.Join(tempDir, "gordon.toml")
600
601 err := os.WriteFile(configFile, []byte(configContent), 0644)
602 require.NoError(t, err)
603
604 viper.Reset()
605 viper.SetConfigFile(configFile)
606 err = viper.ReadInConfig()
607 require.NoError(t, err)
608
609 cfg, err := Load()
610 require.NoError(t, err)
611
612 // Add a new route at the end of the routes section
613 err = cfg.AddRoute("end.example.com", "endapp:v1")
614 require.NoError(t, err)
615
616 // Verify route was added to memory
617 assert.Equal(t, "endapp:v1", cfg.Routes["end.example.com"])
618
619 // Verify config file was updated
620 updatedContent, err := os.ReadFile(configFile)
621 require.NoError(t, err)
622 assert.Contains(t, string(updatedContent), `"end.example.com" = "endapp:v1"`)
623}
624
625func TestValidateEnvConfig_RelativePath(t *testing.T) {
626 envConfig := EnvConfig{
627 Dir: "./relative/path", // relative path should not cause error
628 Providers: []string{"pass"},
629 }
630
631 err := validateEnvConfig(&envConfig)
632 require.NoError(t, err)
633}
634
635func TestValidateEnvConfig_EmptyDir(t *testing.T) {
636 envConfig := EnvConfig{
637 Dir: "", // empty dir should not cause error
638 Providers: []string{"pass"},
639 }
640
641 err := validateEnvConfig(&envConfig)
642 require.NoError(t, err)
643}
644
645func TestGetDefaultDataDir_Coverage(t *testing.T) {
646 // This test mainly exists to ensure coverage of getDefaultDataDir branches
647 // We can't easily test root vs non-root scenarios, but we can test the function
648 dataDir := getDefaultDataDir()
649 assert.NotEmpty(t, dataDir)
650}