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}