1package resolver
2
3import (
4 "context"
5 "errors"
6 "testing"
7
8 "github.com/charmbracelet/crush/internal/env"
9 "github.com/stretchr/testify/assert"
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 assert.Error(t, err)
89 } else {
90 assert.NoError(t, err)
91 assert.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 assert.Error(t, err)
254 } else {
255 assert.NoError(t, err)
256 assert.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 assert.Error(t, err)
310 } else {
311 assert.NoError(t, err)
312 assert.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 assert.NotNil(t, resolver)
323 assert.Implements(t, (*Resolver)(nil), resolver)
324}
325
326func TestNewEnvironmentVariableResolver(t *testing.T) {
327 testEnv := env.NewFromMap(map[string]string{"TEST": "value"})
328 resolver := NewEnvironmentVariableResolver(testEnv)
329
330 assert.NotNil(t, resolver)
331 assert.Implements(t, (*Resolver)(nil), resolver)
332}