v2.0.0 ae3e752

loader_test.go RawHistoryFolder

1package env
2
3import (
4	"os"
5	"path/filepath"
6	"testing"
7
8	"github.com/stretchr/testify/assert"
9	"github.com/stretchr/testify/require"
10
11	"gordon/internal/config"
12)
13
14func TestNewLoader(t *testing.T) {
15	cfg := &config.Config{
16		Env: config.EnvConfig{
17			Dir: "/tmp/env",
18		},
19	}
20
21	loader := NewLoader(cfg)
22
23	assert.NotNil(t, loader)
24	assert.Equal(t, cfg, loader.cfg)
25	assert.NotNil(t, loader.secretProviders)
26	assert.Empty(t, loader.secretProviders)
27}
28
29func TestLoader_RegisterSecretProvider(t *testing.T) {
30	cfg := &config.Config{}
31	loader := NewLoader(cfg)
32
33	mockProvider := &MockSecretProvider{name: "test"}
34	loader.RegisterSecretProvider("test", mockProvider)
35
36	assert.Contains(t, loader.secretProviders, "test")
37	assert.Equal(t, mockProvider, loader.secretProviders["test"])
38}
39
40func TestLoader_LoadEnvForRoute(t *testing.T) {
41	tests := []struct {
42		name           string
43		domain         string
44		fileContent    string
45		expectedVars   []string
46		expectedError  bool
47		setupProvider  bool
48		providerSecret string
49	}{
50		{
51			name:         "no env file exists",
52			domain:       "app.example.com",
53			expectedVars: []string{},
54		},
55		{
56			name:        "basic env file",
57			domain:      "app.example.com",
58			fileContent: "KEY1=value1\nKEY2=value2\n",
59			expectedVars: []string{
60				"KEY1=value1",
61				"KEY2=value2",
62			},
63		},
64		{
65			name: "env file with comments and empty lines",
66			domain: "app.example.com",
67			fileContent: `# This is a comment
68KEY1=value1
69
70# Another comment
71KEY2=value2
72`,
73			expectedVars: []string{
74				"KEY1=value1",
75				"KEY2=value2",
76			},
77		},
78		{
79			name:        "env file with quoted values",
80			domain:      "app.example.com",
81			fileContent: `KEY1="quoted value"` + "\n" + `KEY2='single quoted'` + "\n",
82			expectedVars: []string{
83				"KEY1=quoted value",
84				"KEY2=single quoted",
85			},
86		},
87		{
88			name:           "env file with secret resolution",
89			domain:         "app.example.com",
90			fileContent:    "SECRET_KEY=${test:secret/path}\nNORMAL_KEY=value\n",
91			setupProvider:  true,
92			providerSecret: "secret-value",
93			expectedVars: []string{
94				"SECRET_KEY=secret-value",
95				"NORMAL_KEY=value",
96			},
97		},
98		{
99			name:        "env file with invalid format",
100			domain:      "app.example.com",
101			fileContent: "INVALID_LINE_NO_EQUALS\nVALID_KEY=value\n",
102			expectedVars: []string{
103				"VALID_KEY=value",
104			},
105		},
106		{
107			name:           "env file with unknown secret provider",
108			domain:         "app.example.com",
109			fileContent:    "SECRET_KEY=${unknown:secret/path}\n",
110			expectedError:  true,
111		},
112		{
113			name:           "env file with malformed secret syntax",
114			domain:         "app.example.com",
115			fileContent:    "SECRET_KEY=${malformed\n",
116			expectedError:  true,
117		},
118		{
119			name:           "env file with invalid secret format",
120			domain:         "app.example.com",
121			fileContent:    "SECRET_KEY=${invalid-format}\n",
122			expectedError:  true,
123		},
124		{
125			name:        "domain with special characters",
126			domain:      "sub.app.example.com:8080",
127			fileContent: "KEY1=value1\n",
128			expectedVars: []string{
129				"KEY1=value1",
130			},
131		},
132	}
133
134	for _, tt := range tests {
135		t.Run(tt.name, func(t *testing.T) {
136			envDir := "/tmp/env"
137			cfg := &config.Config{
138				Env: config.EnvConfig{
139					Dir: envDir,
140				},
141			}
142
143			loader := NewLoader(cfg)
144
145			// Setup secret provider if needed
146			if tt.setupProvider {
147				mockProvider := &MockSecretProvider{
148					name:   "test",
149					secret: tt.providerSecret,
150				}
151				loader.RegisterSecretProvider("test", mockProvider)
152			}
153
154			// Create env file if content is provided
155			if tt.fileContent != "" {
156				// Create safe domain name for file
157				safeDomain := "app_example_com"
158				if tt.domain == "sub.app.example.com:8080" {
159					safeDomain = "sub_app_example_com_8080"
160				}
161				
162				// envFile would be: filepath.Join(envDir, safeDomain+".env")
163				
164				// Create directory and file using real filesystem for this test
165				realEnvDir := filepath.Join(os.TempDir(), "gordon-test-env")
166				defer os.RemoveAll(realEnvDir)
167				
168				err := os.MkdirAll(realEnvDir, 0755)
169				require.NoError(t, err)
170				
171				realEnvFile := filepath.Join(realEnvDir, safeDomain+".env")
172				err = os.WriteFile(realEnvFile, []byte(tt.fileContent), 0644)
173				require.NoError(t, err)
174				
175				// Update config to use real directory
176				cfg.Env.Dir = realEnvDir
177			}
178
179			// Execute test
180			vars, err := loader.LoadEnvForRoute(tt.domain)
181
182			// Verify results
183			if tt.expectedError {
184				assert.Error(t, err)
185			} else {
186				assert.NoError(t, err)
187				assert.Equal(t, tt.expectedVars, vars)
188			}
189		})
190	}
191}
192
193func TestLoader_ResolveSecrets(t *testing.T) {
194	cfg := &config.Config{}
195	loader := NewLoader(cfg)
196
197	// Register mock secret provider
198	mockProvider := &MockSecretProvider{
199		name:   "test",
200		secret: "secret-value",
201	}
202	loader.RegisterSecretProvider("test", mockProvider)
203
204	tests := []struct {
205		name          string
206		input         string
207		expected      string
208		expectedError bool
209	}{
210		{
211			name:     "no secret syntax",
212			input:    "plain-value",
213			expected: "plain-value",
214		},
215		{
216			name:     "single secret replacement",
217			input:    "${test:secret/path}",
218			expected: "secret-value",
219		},
220		{
221			name:     "secret in middle of string",
222			input:    "prefix-${test:secret/path}-suffix",
223			expected: "prefix-secret-value-suffix",
224		},
225		{
226			name:     "multiple secrets",
227			input:    "${test:secret1}-${test:secret2}",
228			expected: "secret-value-secret-value",
229		},
230		{
231			name:          "unclosed secret syntax",
232			input:         "${test:secret",
233			expectedError: true,
234		},
235		{
236			name:          "invalid secret format",
237			input:         "${invalid-format}",
238			expectedError: true,
239		},
240		{
241			name:          "unknown provider",
242			input:         "${unknown:secret/path}",
243			expectedError: true,
244		},
245	}
246
247	for _, tt := range tests {
248		t.Run(tt.name, func(t *testing.T) {
249			result, err := loader.resolveSecrets(tt.input)
250
251			if tt.expectedError {
252				assert.Error(t, err)
253			} else {
254				assert.NoError(t, err)
255				assert.Equal(t, tt.expected, result)
256			}
257		})
258	}
259}
260
261func TestLoader_EnsureEnvDir(t *testing.T) {
262	tempDir := os.TempDir()
263	envDir := filepath.Join(tempDir, "gordon-test-env-ensure")
264	defer os.RemoveAll(envDir)
265
266	cfg := &config.Config{
267		Env: config.EnvConfig{
268			Dir: envDir,
269		},
270	}
271
272	loader := NewLoader(cfg)
273
274	// Directory should not exist initially
275	_, err := os.Stat(envDir)
276	assert.True(t, os.IsNotExist(err))
277
278	// Ensure directory
279	err = loader.EnsureEnvDir()
280	assert.NoError(t, err)
281
282	// Directory should now exist
283	info, err := os.Stat(envDir)
284	assert.NoError(t, err)
285	assert.True(t, info.IsDir())
286	assert.Equal(t, os.FileMode(0700), info.Mode().Perm())
287}
288
289func TestLoader_CreateEnvFilesForRoutes(t *testing.T) {
290	tempDir := os.TempDir()
291	envDir := filepath.Join(tempDir, "gordon-test-env-create")
292	defer os.RemoveAll(envDir)
293
294	cfg := &config.Config{
295		Env: config.EnvConfig{
296			Dir: envDir,
297		},
298		Routes: map[string]string{
299			"app1.example.com":      "nginx:latest",
300			"app2.example.com":      "redis:alpine",
301			"api.example.com:8080":  "myapi:v1",
302		},
303	}
304
305	loader := NewLoader(cfg)
306
307	// Ensure the env directory exists
308	err := os.MkdirAll(envDir, 0755)
309	require.NoError(t, err)
310
311	err = loader.CreateEnvFilesForRoutes()
312	assert.NoError(t, err)
313
314	// Check that files were created
315	expectedFiles := map[string]struct{domain, image string}{
316		"app1_example_com.env":     {domain: "app1.example.com", image: "nginx:latest"},
317		"app2_example_com.env":     {domain: "app2.example.com", image: "redis:alpine"},
318		"api_example_com_8080.env": {domain: "api.example.com:8080", image: "myapi:v1"},
319	}
320
321	for fileName, expected := range expectedFiles {
322		filePath := filepath.Join(envDir, fileName)
323		assert.FileExists(t, filePath)
324
325		// Check file content
326		content, err := os.ReadFile(filePath)
327		assert.NoError(t, err)
328		
329		expectedContent := "# Environment variables for route: " + expected.domain + "\n# Image: " + expected.image + "\n\n"
330		assert.Contains(t, string(content), expectedContent)
331
332		// Check file permissions
333		info, err := os.Stat(filePath)
334		assert.NoError(t, err)
335		assert.Equal(t, os.FileMode(0600), info.Mode().Perm())
336	}
337}
338
339func TestLoader_CreateEnvFilesForRoutes_NoRoutes(t *testing.T) {
340	cfg := &config.Config{
341		Env: config.EnvConfig{
342			Dir: "/tmp/env",
343		},
344		Routes: map[string]string{},
345	}
346
347	loader := NewLoader(cfg)
348
349	err := loader.CreateEnvFilesForRoutes()
350	assert.NoError(t, err)
351}
352
353func TestLoader_CreateEnvFilesForRoutes_ExistingFile(t *testing.T) {
354	tempDir := os.TempDir()
355	envDir := filepath.Join(tempDir, "gordon-test-env-existing")
356	defer os.RemoveAll(envDir)
357
358	err := os.MkdirAll(envDir, 0755)
359	require.NoError(t, err)
360
361	// Create existing file
362	existingFile := filepath.Join(envDir, "app_example_com.env")
363	err = os.WriteFile(existingFile, []byte("EXISTING=value\n"), 0644)
364	require.NoError(t, err)
365
366	cfg := &config.Config{
367		Env: config.EnvConfig{
368			Dir: envDir,
369		},
370		Routes: map[string]string{
371			"app.example.com": "nginx:latest",
372		},
373	}
374
375	loader := NewLoader(cfg)
376
377	err = loader.CreateEnvFilesForRoutes()
378	assert.NoError(t, err)
379
380	// File should still have original content
381	content, err := os.ReadFile(existingFile)
382	assert.NoError(t, err)
383	assert.Equal(t, "EXISTING=value\n", string(content))
384}
385
386func TestLoader_CreateEnvFileForRoute(t *testing.T) {
387	tempDir := os.TempDir()
388	envDir := filepath.Join(tempDir, "gordon-test-env-single")
389	defer os.RemoveAll(envDir)
390
391	cfg := &config.Config{
392		Env: config.EnvConfig{
393			Dir: envDir,
394		},
395	}
396
397	loader := NewLoader(cfg)
398
399	domain := "test.example.com"
400	image := "test:latest"
401
402	err := loader.CreateEnvFileForRoute(domain, image)
403	assert.NoError(t, err)
404
405	// Check file was created
406	expectedFile := filepath.Join(envDir, "test_example_com.env")
407	assert.FileExists(t, expectedFile)
408
409	// Check content
410	content, err := os.ReadFile(expectedFile)
411	assert.NoError(t, err)
412	
413	expectedContent := "# Environment variables for route: test.example.com\n# Image: test:latest\n\n"
414	assert.Equal(t, expectedContent, string(content))
415
416	// Test creating the same file again (should skip)
417	err = loader.CreateEnvFileForRoute(domain, image)
418	assert.NoError(t, err)
419
420	// Content should be unchanged
421	content2, err := os.ReadFile(expectedFile)
422	assert.NoError(t, err)
423	assert.Equal(t, string(content), string(content2))
424}
425
426func TestLoader_UpdateConfig(t *testing.T) {
427	cfg1 := &config.Config{Env: config.EnvConfig{Dir: "/tmp/env1"}}
428	cfg2 := &config.Config{Env: config.EnvConfig{Dir: "/tmp/env2"}}
429
430	loader := NewLoader(cfg1)
431	assert.Equal(t, cfg1, loader.cfg)
432
433	loader.UpdateConfig(cfg2)
434	assert.Equal(t, cfg2, loader.cfg)
435}
436
437func TestLoader_GetEnvFilePath(t *testing.T) {
438	cfg := &config.Config{
439		Env: config.EnvConfig{
440			Dir: "/tmp/env",
441		},
442	}
443
444	loader := NewLoader(cfg)
445
446	tests := []struct {
447		domain   string
448		expected string
449	}{
450		{
451			domain:   "app.example.com",
452			expected: "/tmp/env/app_example_com.env",
453		},
454		{
455			domain:   "api.example.com:8080",
456			expected: "/tmp/env/api_example_com_8080.env",
457		},
458		{
459			domain:   "sub.domain.example.com/path",
460			expected: "/tmp/env/sub_domain_example_com_path.env",
461		},
462	}
463
464	for _, tt := range tests {
465		t.Run(tt.domain, func(t *testing.T) {
466			result := loader.GetEnvFilePath(tt.domain)
467			assert.Equal(t, tt.expected, result)
468		})
469	}
470}
471
472// MockSecretProvider is a mock implementation of SecretProvider for testing
473type MockSecretProvider struct {
474	name   string
475	secret string
476	err    error
477}
478
479func (m *MockSecretProvider) Name() string {
480	return m.name
481}
482
483func (m *MockSecretProvider) GetSecret(path string) (string, error) {
484	if m.err != nil {
485		return "", m.err
486	}
487	return m.secret, nil
488}

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