1package config
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/stretchr/testify/require"
9)
10
11// mockShell implements the Shell interface for testing
12type mockShell struct {
13 execFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
14}
15
16func (m *mockShell) Exec(ctx context.Context, command string) (stdout, stderr string, err error) {
17 if m.execFunc != nil {
18 return m.execFunc(ctx, command)
19 }
20 return "", "", nil
21}
22
23func TestShellVariableResolver_ResolveValue(t *testing.T) {
24 tests := []struct {
25 name string
26 value string
27 envVars []string
28 shellFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
29 expected string
30 expectError bool
31 }{
32 {
33 name: "non-variable string returns as-is",
34 value: "plain-string",
35 expected: "plain-string",
36 },
37 {
38 name: "environment variable resolution",
39 value: "$HOME",
40 envVars: []string{"HOME=/home/user"},
41 expected: "/home/user",
42 },
43 {
44 name: "missing environment variable returns error",
45 value: "$MISSING_VAR",
46 envVars: []string{},
47 expectError: true,
48 },
49
50 {
51 name: "shell command with whitespace trimming",
52 value: "$(echo ' spaced ')",
53 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
54 if command == "echo ' spaced '" {
55 return " spaced \n", "", nil
56 }
57 return "", "", errors.New("unexpected command")
58 },
59 expected: "spaced",
60 },
61 {
62 name: "shell command execution error",
63 value: "$(false)",
64 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
65 return "", "", errors.New("command failed")
66 },
67 expectError: true,
68 },
69 {
70 name: "invalid format returns error",
71 value: "$",
72 expectError: true,
73 },
74 }
75
76 for _, tt := range tests {
77 t.Run(tt.name, func(t *testing.T) {
78 testEnv := environ(tt.envVars)
79 resolver := &shellVariableResolver{
80 shell: &mockShell{execFunc: tt.shellFunc},
81 env: testEnv,
82 }
83
84 result, err := resolver.ResolveValue(tt.value)
85
86 if tt.expectError {
87 require.Error(t, err)
88 } else {
89 require.NoError(t, err)
90 require.Equal(t, tt.expected, result)
91 }
92 })
93 }
94}
95
96func TestShellVariableResolver_EnhancedResolveValue(t *testing.T) {
97 tests := []struct {
98 name string
99 value string
100 envVars []string
101 shellFunc func(ctx context.Context, command string) (stdout, stderr string, err error)
102 expected string
103 expectError bool
104 }{
105 {
106 name: "command substitution within string",
107 value: "Bearer $(echo token123)",
108 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
109 if command == "echo token123" {
110 return "token123\n", "", nil
111 }
112 return "", "", errors.New("unexpected command")
113 },
114 expected: "Bearer token123",
115 },
116 {
117 name: "environment variable within string",
118 value: "Bearer $TOKEN",
119 envVars: []string{"TOKEN=sk-ant-123"},
120 expected: "Bearer sk-ant-123",
121 },
122 {
123 name: "environment variable with braces within string",
124 value: "Bearer ${TOKEN}",
125 envVars: []string{"TOKEN=sk-ant-456"},
126 expected: "Bearer sk-ant-456",
127 },
128 {
129 name: "mixed command and environment substitution",
130 value: "$USER-$(date +%Y)-$HOST",
131 envVars: []string{
132 "USER=testuser",
133 "HOST=localhost",
134 },
135 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
136 if command == "date +%Y" {
137 return "2024\n", "", nil
138 }
139 return "", "", errors.New("unexpected command")
140 },
141 expected: "testuser-2024-localhost",
142 },
143 {
144 name: "multiple command substitutions",
145 value: "$(echo hello) $(echo world)",
146 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
147 switch command {
148 case "echo hello":
149 return "hello\n", "", nil
150 case "echo world":
151 return "world\n", "", nil
152 }
153 return "", "", errors.New("unexpected command")
154 },
155 expected: "hello world",
156 },
157 {
158 name: "nested parentheses in command",
159 value: "$(echo $(echo inner))",
160 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
161 if command == "echo $(echo inner)" {
162 return "nested\n", "", nil
163 }
164 return "", "", errors.New("unexpected command")
165 },
166 expected: "nested",
167 },
168 {
169 name: "lone dollar with non-variable chars",
170 value: "prefix$123suffix", // Numbers can't start variable names
171 expectError: true,
172 },
173 {
174 name: "dollar with special chars",
175 value: "a$@b$#c", // Special chars aren't valid in variable names
176 expectError: true,
177 },
178 {
179 name: "empty environment variable substitution",
180 value: "Bearer $EMPTY_VAR",
181 envVars: []string{},
182 expectError: true,
183 },
184 {
185 name: "unmatched command substitution opening",
186 value: "Bearer $(echo test",
187 expectError: true,
188 },
189 {
190 name: "unmatched environment variable braces",
191 value: "Bearer ${TOKEN",
192 expectError: true,
193 },
194 {
195 name: "command substitution with error",
196 value: "Bearer $(false)",
197 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
198 return "", "", errors.New("command failed")
199 },
200 expectError: true,
201 },
202 {
203 name: "complex real-world example",
204 value: "Bearer $(cat /tmp/token.txt | base64 -w 0)",
205 shellFunc: func(ctx context.Context, command string) (stdout, stderr string, err error) {
206 if command == "cat /tmp/token.txt | base64 -w 0" {
207 return "c2stYW50LXRlc3Q=\n", "", nil
208 }
209 return "", "", errors.New("unexpected command")
210 },
211 expected: "Bearer c2stYW50LXRlc3Q=",
212 },
213 {
214 name: "environment variable with underscores and numbers",
215 value: "Bearer $API_KEY_V2",
216 envVars: []string{"API_KEY_V2=sk-test-123"},
217 expected: "Bearer sk-test-123",
218 },
219 {
220 name: "no substitution needed",
221 value: "Bearer sk-ant-static-token",
222 expected: "Bearer sk-ant-static-token",
223 },
224 {
225 name: "incomplete variable at end",
226 value: "Bearer $",
227 expectError: true,
228 },
229 {
230 name: "variable with invalid character",
231 value: "Bearer $VAR-NAME", // Hyphen not allowed in variable names
232 expectError: true,
233 },
234 {
235 name: "multiple invalid variables",
236 value: "$1$2$3",
237 expectError: true,
238 },
239 }
240
241 for _, tt := range tests {
242 t.Run(tt.name, func(t *testing.T) {
243 testEnv := environ(tt.envVars)
244 resolver := &shellVariableResolver{
245 shell: &mockShell{execFunc: tt.shellFunc},
246 env: testEnv,
247 }
248
249 result, err := resolver.ResolveValue(tt.value)
250
251 if tt.expectError {
252 require.Error(t, err)
253 } else {
254 require.NoError(t, err)
255 require.Equal(t, tt.expected, result)
256 }
257 })
258 }
259}
260
261func TestEnvironmentVariableResolver_ResolveValue(t *testing.T) {
262 tests := []struct {
263 name string
264 value string
265 envVars []string
266 expected string
267 expectError bool
268 }{
269 {
270 name: "non-variable string returns as-is",
271 value: "plain-string",
272 expected: "plain-string",
273 },
274 {
275 name: "environment variable resolution",
276 value: "$HOME",
277 envVars: []string{"HOME=/home/user"},
278 expected: "/home/user",
279 },
280 {
281 name: "environment variable with complex value",
282 value: "$PATH",
283 envVars: []string{"PATH=/usr/bin:/bin:/usr/local/bin"},
284 expected: "/usr/bin:/bin:/usr/local/bin",
285 },
286 {
287 name: "missing environment variable returns error",
288 value: "$MISSING_VAR",
289 envVars: []string{},
290 expectError: true,
291 },
292 {
293 name: "empty environment variable returns error",
294 value: "$EMPTY_VAR",
295 envVars: []string{"EMPTY_VAR="},
296 expectError: true,
297 },
298 }
299
300 for _, tt := range tests {
301 t.Run(tt.name, func(t *testing.T) {
302 testEnv := environ(tt.envVars)
303 resolver := NewEnvironmentVariableResolver(testEnv)
304
305 result, err := resolver.ResolveValue(tt.value)
306
307 if tt.expectError {
308 require.Error(t, err)
309 } else {
310 require.NoError(t, err)
311 require.Equal(t, tt.expected, result)
312 }
313 })
314 }
315}
316
317func TestNewShellVariableResolver(t *testing.T) {
318 testEnv := environ([]string{"TEST=value"})
319 resolver := NewShellVariableResolver(testEnv)
320
321 require.NotNil(t, resolver)
322 require.Implements(t, (*VariableResolver)(nil), resolver)
323}
324
325func TestNewEnvironmentVariableResolver(t *testing.T) {
326 testEnv := environ([]string{"TEST=value"})
327 resolver := NewEnvironmentVariableResolver(testEnv)
328
329 require.NotNil(t, resolver)
330 require.Implements(t, (*VariableResolver)(nil), resolver)
331}