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