resolve_test.go 8.8 KB

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