v2.0.0 ae3e752

config_test.go RawHistoryFolder

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}

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 7

Tags 106

↑↓ moveenter openesc close