v2.2.7 aefe19e

file_test.go RawHistoryFolder

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}

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