1package envloader
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8
9 "github.com/bnema/zerowrap"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12
13 "gordon/internal/boundaries/out/mocks"
14)
15
16func TestFileLoader_ResolveSecrets(t *testing.T) {
17 // Create a temp directory for tests
18 tmpDir := t.TempDir()
19 log := zerowrap.New(zerowrap.Config{Level: "fatal"})
20
21 loader, err := NewFileLoader(tmpDir, log)
22 require.NoError(t, err)
23
24 ctx := context.Background()
25
26 t.Run("no secret syntax returns value unchanged", func(t *testing.T) {
27 result, err := loader.resolveSecrets(ctx, "plain-value")
28 assert.NoError(t, err)
29 assert.Equal(t, "plain-value", result)
30 })
31
32 t.Run("unclosed secret syntax returns error", func(t *testing.T) {
33 _, err := loader.resolveSecrets(ctx, "${pass:secret")
34 assert.Error(t, err)
35 assert.Contains(t, err.Error(), "unclosed secret syntax")
36 })
37
38 t.Run("invalid secret syntax without colon returns error", func(t *testing.T) {
39 _, err := loader.resolveSecrets(ctx, "${invalid}")
40 assert.Error(t, err)
41 assert.Contains(t, err.Error(), "invalid secret syntax")
42 })
43
44 t.Run("unknown provider returns error", func(t *testing.T) {
45 _, err := loader.resolveSecrets(ctx, "${unknown:path}")
46 assert.Error(t, err)
47 assert.Contains(t, err.Error(), "unknown secret provider")
48 })
49
50 t.Run("pass provider resolves secret", func(t *testing.T) {
51 mockPass := mocks.NewMockSecretProvider(t)
52 mockPass.EXPECT().Name().Return("pass")
53 mockPass.EXPECT().GetSecret(ctx, "company/api-key").Return("secret-value-123", nil)
54
55 loader.RegisterSecretProvider(mockPass)
56
57 result, err := loader.resolveSecrets(ctx, "${pass:company/api-key}")
58 assert.NoError(t, err)
59 assert.Equal(t, "secret-value-123", result)
60 })
61
62 t.Run("sops provider resolves secret", func(t *testing.T) {
63 mockSops := mocks.NewMockSecretProvider(t)
64 mockSops.EXPECT().Name().Return("sops")
65 mockSops.EXPECT().GetSecret(ctx, "secrets.yaml:app.password").Return("sops-secret", nil)
66
67 loader.RegisterSecretProvider(mockSops)
68
69 result, err := loader.resolveSecrets(ctx, "${sops:secrets.yaml:app.password}")
70 assert.NoError(t, err)
71 assert.Equal(t, "sops-secret", result)
72 })
73
74 t.Run("multiple secrets in one value", func(t *testing.T) {
75 mockPass := mocks.NewMockSecretProvider(t)
76 mockPass.EXPECT().Name().Return("pass")
77 mockPass.EXPECT().GetSecret(ctx, "db/user").Return("admin", nil)
78 mockPass.EXPECT().GetSecret(ctx, "db/pass").Return("secret123", nil)
79
80 loader.RegisterSecretProvider(mockPass)
81
82 result, err := loader.resolveSecrets(ctx, "postgresql://${pass:db/user}:${pass:db/pass}@localhost:5432/db")
83 assert.NoError(t, err)
84 assert.Equal(t, "postgresql://admin:secret123@localhost:5432/db", result)
85 })
86
87 t.Run("provider error is propagated", func(t *testing.T) {
88 mockPass := mocks.NewMockSecretProvider(t)
89 mockPass.EXPECT().Name().Return("pass")
90 mockPass.EXPECT().GetSecret(ctx, "missing/secret").Return("", assert.AnError)
91
92 loader.RegisterSecretProvider(mockPass)
93
94 _, err := loader.resolveSecrets(ctx, "${pass:missing/secret}")
95 assert.Error(t, err)
96 assert.Contains(t, err.Error(), "failed to get secret from provider")
97 })
98}
99
100func TestFileLoader_LoadEnv(t *testing.T) {
101 log := zerowrap.New(zerowrap.Config{Level: "fatal"})
102 ctx := context.Background()
103
104 t.Run("missing env file returns empty slice", func(t *testing.T) {
105 tmpDir := t.TempDir()
106 loader, err := NewFileLoader(tmpDir, log)
107 require.NoError(t, err)
108
109 envVars, err := loader.LoadEnv(ctx, "nonexistent.example.com")
110 assert.NoError(t, err)
111 assert.Empty(t, envVars)
112 })
113
114 t.Run("loads simple env file", func(t *testing.T) {
115 tmpDir := t.TempDir()
116 loader, err := NewFileLoader(tmpDir, log)
117 require.NoError(t, err)
118
119 // Create env file
120 envContent := `# Comment line
121NODE_ENV=production
122API_URL=https://api.example.com
123`
124 envFile := filepath.Join(tmpDir, "app_example_com.env")
125 require.NoError(t, os.WriteFile(envFile, []byte(envContent), 0600))
126
127 envVars, err := loader.LoadEnv(ctx, "app.example.com")
128 assert.NoError(t, err)
129 assert.Len(t, envVars, 2)
130 assert.Contains(t, envVars, "NODE_ENV=production")
131 assert.Contains(t, envVars, "API_URL=https://api.example.com")
132 })
133
134 t.Run("loads env file with secrets", func(t *testing.T) {
135 tmpDir := t.TempDir()
136 loader, err := NewFileLoader(tmpDir, log)
137 require.NoError(t, err)
138
139 // Register mock provider
140 mockPass := mocks.NewMockSecretProvider(t)
141 mockPass.EXPECT().Name().Return("pass")
142 mockPass.EXPECT().GetSecret(ctx, "app/api-key").Return("super-secret-key", nil)
143 loader.RegisterSecretProvider(mockPass)
144
145 // Create env file with secret reference
146 envContent := `NODE_ENV=production
147API_KEY=${pass:app/api-key}
148`
149 envFile := filepath.Join(tmpDir, "app_example_com.env")
150 require.NoError(t, os.WriteFile(envFile, []byte(envContent), 0600))
151
152 envVars, err := loader.LoadEnv(ctx, "app.example.com")
153 assert.NoError(t, err)
154 assert.Len(t, envVars, 2)
155 assert.Contains(t, envVars, "NODE_ENV=production")
156 assert.Contains(t, envVars, "API_KEY=super-secret-key")
157 })
158
159 t.Run("handles quoted values", func(t *testing.T) {
160 tmpDir := t.TempDir()
161 loader, err := NewFileLoader(tmpDir, log)
162 require.NoError(t, err)
163
164 envContent := `DOUBLE_QUOTED="hello world"
165SINGLE_QUOTED='hello world'
166`
167 envFile := filepath.Join(tmpDir, "app_example_com.env")
168 require.NoError(t, os.WriteFile(envFile, []byte(envContent), 0600))
169
170 envVars, err := loader.LoadEnv(ctx, "app.example.com")
171 assert.NoError(t, err)
172 assert.Len(t, envVars, 2)
173 assert.Contains(t, envVars, "DOUBLE_QUOTED=hello world")
174 assert.Contains(t, envVars, "SINGLE_QUOTED=hello world")
175 })
176
177 t.Run("skips invalid lines", func(t *testing.T) {
178 tmpDir := t.TempDir()
179 loader, err := NewFileLoader(tmpDir, log)
180 require.NoError(t, err)
181
182 envContent := `VALID=value
183invalid line without equals
184ALSO_VALID=another
185`
186 envFile := filepath.Join(tmpDir, "app_example_com.env")
187 require.NoError(t, os.WriteFile(envFile, []byte(envContent), 0600))
188
189 envVars, err := loader.LoadEnv(ctx, "app.example.com")
190 assert.NoError(t, err)
191 assert.Len(t, envVars, 2)
192 assert.Contains(t, envVars, "VALID=value")
193 assert.Contains(t, envVars, "ALSO_VALID=another")
194 })
195}
196
197func TestFileLoader_GetEnvFilePath(t *testing.T) {
198 tmpDir := t.TempDir()
199 log := zerowrap.New(zerowrap.Config{Level: "fatal"})
200
201 loader, err := NewFileLoader(tmpDir, log)
202 require.NoError(t, err)
203
204 tests := []struct {
205 domain string
206 expected string
207 }{
208 {"app.example.com", "app_example_com.env"},
209 {"api.sub.example.com", "api_sub_example_com.env"},
210 {"localhost:8080", "localhost_8080.env"},
211 }
212
213 for _, tt := range tests {
214 t.Run(tt.domain, func(t *testing.T) {
215 result := loader.getEnvFilePath(tt.domain)
216 assert.Equal(t, filepath.Join(tmpDir, tt.expected), result)
217 })
218 }
219}