resolve_test.go

  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}