4
0

arguments_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583
  1. package executor
  2. import (
  3. "fmt"
  4. "strings"
  5. config "github.com/OliveTin/OliveTin/internal/config"
  6. "github.com/stretchr/testify/assert"
  7. "testing"
  8. )
  9. func TestSanitizeUnsafe(t *testing.T) {
  10. assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
  11. }
  12. func TestSanitizeUnimplemented(t *testing.T) {
  13. err := TypeSafetyCheck("", "I am a happy little argument", "greeting_type")
  14. assert.NotNil(t, err, "Test an argument type that does not exist")
  15. }
  16. func TestArgumentValueNullable(t *testing.T) {
  17. a1 := config.Action{
  18. Title: "Release the hounds",
  19. Shell: "echo 'Releasing {{ count }} hounds'",
  20. Arguments: []config.ActionArgument{
  21. {
  22. Name: "count",
  23. Type: "int",
  24. },
  25. },
  26. }
  27. values := map[string]string{
  28. "count": "",
  29. }
  30. out, err := parseActionArguments(values, &a1, "")
  31. assert.Equal(t, "echo 'Releasing hounds'", out)
  32. assert.Nil(t, err)
  33. a1.Arguments[0].RejectNull = true
  34. _, err = parseActionArguments(values, &a1, "")
  35. assert.NotNil(t, err)
  36. }
  37. func TestArgumentNameNumbers(t *testing.T) {
  38. a1 := config.Action{
  39. Title: "Do some tickles",
  40. Shell: "echo 'Tickling {{ person1name }}'",
  41. Arguments: []config.ActionArgument{
  42. {
  43. Name: "person1name",
  44. Type: "ascii",
  45. },
  46. },
  47. }
  48. values := map[string]string{
  49. "person1name": "Fred",
  50. }
  51. out, err := parseActionArguments(values, &a1, "")
  52. assert.Equal(t, "echo 'Tickling Fred'", out)
  53. assert.Nil(t, err)
  54. }
  55. func TestArgumentNotProvided(t *testing.T) {
  56. a1 := config.Action{
  57. Title: "Do some tickles",
  58. Shell: "echo 'Tickling {{ personName }}'",
  59. Arguments: []config.ActionArgument{
  60. {
  61. Name: "person",
  62. Type: "ascii",
  63. },
  64. },
  65. }
  66. values := map[string]string{}
  67. out, err := parseActionArguments(values, &a1, "")
  68. assert.Equal(t, "", out)
  69. assert.Equal(t, err.Error(), "required arg not provided: personName")
  70. }
  71. func TestTypeSafetyCheckUrl(t *testing.T) {
  72. assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
  73. assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
  74. assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
  75. assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
  76. assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
  77. assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
  78. }
  79. func TestTypeSafetyCheckRegex(t *testing.T) {
  80. tests := []struct {
  81. name string
  82. field string
  83. pattern string
  84. value string
  85. hasError bool
  86. }{
  87. {
  88. name: "Issue #578 - Domain",
  89. field: "domain",
  90. pattern: "regex:^(?:[a-zA-Z0-9-]{1,63}.)+[a-zA-Z]{2,63}$",
  91. value: "immich.example.dev",
  92. hasError: false,
  93. },
  94. {
  95. name: "Don't allow numbers in username",
  96. field: "Username",
  97. pattern: "regex:^[a-zA-Z]$",
  98. value: "James1234",
  99. hasError: true,
  100. },
  101. }
  102. for _, tt := range tests {
  103. t.Run(tt.name, func(t *testing.T) {
  104. err := typeSafetyCheckRegex(tt.field, tt.value, tt.pattern)
  105. if tt.hasError {
  106. assert.NotNil(t, err, "Expected error for value %s with pattern %s, but got no error", tt.value, tt.pattern)
  107. } else {
  108. assert.Nil(t, err, "Expected no error for value %s with pattern %s, but got error: %v", tt.value, tt.pattern, err)
  109. }
  110. })
  111. }
  112. }
  113. func TestRedactShellCommand(t *testing.T) {
  114. cmd := "echo 'The password for Fred is toomanysecrets'"
  115. args := []config.ActionArgument{
  116. {
  117. Name: "personName",
  118. Type: "ascii",
  119. },
  120. {
  121. Name: "password",
  122. Type: "password",
  123. },
  124. }
  125. values := map[string]string{
  126. "personName": "Fred",
  127. "password": "toomanysecrets",
  128. }
  129. res := redactShellCommand(cmd, args, values)
  130. assert.Equal(t, "echo 'The password for Fred is <redacted>'", res, "Redacted shell command should mask the password argument")
  131. // Test with empty password
  132. values["password"] = ""
  133. res = redactShellCommand(cmd, args, values)
  134. assert.Equal(t, cmd, res, "Empty password should not change the command")
  135. // Test with missing password argument
  136. delete(values, "password")
  137. res = redactShellCommand(cmd, args, values)
  138. assert.Equal(t, cmd, res, "Missing password argument should not change the command")
  139. }
  140. func TestTypeSafetyCheckEmail(t *testing.T) {
  141. tests := []struct {
  142. name string
  143. field string
  144. value string
  145. hasError bool
  146. }{
  147. {"Valid simple email", "email", "user@example.com", false},
  148. {"Valid email with subdomain", "email", "user@mail.example.com", false},
  149. {"Valid email with plus", "email", "user+test@example.com", false},
  150. {"Valid email with dash", "email", "user-name@example.com", false},
  151. {"Valid email with numbers", "email", "user123@example123.com", false},
  152. {"Invalid email no @", "email", "userexample.com", true},
  153. {"Invalid email no domain", "email", "user@", true},
  154. {"Invalid email no user", "email", "@example.com", true},
  155. {"Invalid email spaces", "email", "user name@example.com", true},
  156. {"Invalid email double @", "email", "user@@example.com", true},
  157. }
  158. for _, tt := range tests {
  159. t.Run(tt.name, func(t *testing.T) {
  160. err := TypeSafetyCheck(tt.field, tt.value, "email")
  161. if tt.hasError {
  162. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  163. } else {
  164. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  165. }
  166. })
  167. }
  168. }
  169. func TestTypeSafetyCheckDatetime(t *testing.T) {
  170. tests := []struct {
  171. name string
  172. field string
  173. value string
  174. hasError bool
  175. }{
  176. {"Valid datetime", "datetime", "2023-12-25T15:30:45", false},
  177. {"Valid datetime morning", "datetime", "2023-01-01T00:00:00", false},
  178. {"Valid datetime evening", "datetime", "2023-12-31T23:59:59", false},
  179. {"Invalid format missing T", "datetime", "2023-12-25 15:30:45", true},
  180. {"Invalid format missing seconds", "datetime", "2023-12-25T15:30", true},
  181. {"Invalid date", "datetime", "2023-13-25T15:30:45", true},
  182. {"Invalid time", "datetime", "2023-12-25T25:30:45", true},
  183. {"Random string", "datetime", "not-a-date", true},
  184. }
  185. for _, tt := range tests {
  186. t.Run(tt.name, func(t *testing.T) {
  187. err := TypeSafetyCheck(tt.field, tt.value, "datetime")
  188. if tt.hasError {
  189. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  190. } else {
  191. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  192. }
  193. })
  194. }
  195. }
  196. func TestTypeSafetyCheckRawStringMultiline(t *testing.T) {
  197. tests := []struct {
  198. name string
  199. field string
  200. value string
  201. }{
  202. {"Simple string", "content", "hello world"},
  203. {"Multiline string", "content", "line1\nline2\nline3"},
  204. {"String with special chars", "content", "!@#$%^&*()"},
  205. {"Unicode string", "content", "héllo wörld 🌍"},
  206. {"Very long string", "content", strings.Repeat("a", 1000)},
  207. }
  208. for _, tt := range tests {
  209. t.Run(tt.name, func(t *testing.T) {
  210. err := TypeSafetyCheck(tt.field, tt.value, "raw_string_multiline")
  211. assert.Nil(t, err, "raw_string_multiline should accept any value")
  212. })
  213. }
  214. }
  215. func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) {
  216. tests := []struct {
  217. name string
  218. field string
  219. value string
  220. hasError bool
  221. }{
  222. {"Valid unicode identifier", "name", "hello_world", false},
  223. {"Valid with numbers", "name", "test123", false},
  224. {"Valid with spaces", "name", "hello world", false},
  225. {"Valid with path separators", "name", "path/to/file", false},
  226. {"Valid with backslashes", "name", "path\\to\\file", false},
  227. {"Valid with dots", "name", "file.txt", false},
  228. {"Valid with underscores", "name", "my_file_name", false},
  229. {"Invalid with special chars", "name", "hello@world", true},
  230. {"Invalid with brackets", "name", "hello[world]", true},
  231. }
  232. for _, tt := range tests {
  233. t.Run(tt.name, func(t *testing.T) {
  234. err := TypeSafetyCheck(tt.field, tt.value, "unicode_identifier")
  235. if tt.hasError {
  236. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  237. } else {
  238. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  239. }
  240. })
  241. }
  242. }
  243. func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
  244. tests := []struct {
  245. name string
  246. field string
  247. value string
  248. hasError bool
  249. }{
  250. {"Valid identifier", "name", "hello_world", false},
  251. {"Valid with numbers", "name", "test123", false},
  252. {"Valid with dots", "name", "file.txt", false},
  253. {"Valid with dashes", "name", "my-file", false},
  254. {"Valid with underscores", "name", "my_file", false},
  255. {"Invalid with spaces", "name", "hello world", true},
  256. {"Invalid with special chars", "name", "hello@world", true},
  257. {"Invalid unicode", "name", "héllo", true},
  258. }
  259. for _, tt := range tests {
  260. t.Run(tt.name, func(t *testing.T) {
  261. err := TypeSafetyCheck(tt.field, tt.value, "ascii_identifier")
  262. if tt.hasError {
  263. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  264. } else {
  265. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  266. }
  267. })
  268. }
  269. }
  270. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  271. tests := []struct {
  272. name string
  273. field string
  274. value string
  275. hasError bool
  276. }{
  277. {"Valid sentence", "text", "Hello world", false},
  278. {"Valid with numbers", "text", "Test 123", false},
  279. {"Valid with commas", "text", "Hello, world", false},
  280. {"Valid with periods", "text", "Hello world.", false},
  281. {"Valid with multiple spaces", "text", "Hello world", false},
  282. {"Invalid with special chars", "text", "Hello@world", true},
  283. {"Invalid with parentheses", "text", "Hello (world)", true},
  284. {"Invalid unicode", "text", "Héllo world", true},
  285. }
  286. for _, tt := range tests {
  287. t.Run(tt.name, func(t *testing.T) {
  288. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  289. if tt.hasError {
  290. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  291. } else {
  292. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  293. }
  294. })
  295. }
  296. }
  297. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  298. arg := config.ActionArgument{
  299. Name: "",
  300. Type: "ascii",
  301. }
  302. action := config.Action{Title: "Test"}
  303. err := typecheckActionArgument(&arg, "test", &action)
  304. assert.NotNil(t, err)
  305. assert.Contains(t, err.Error(), "argument name cannot be empty")
  306. }
  307. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  308. arg := config.ActionArgument{
  309. Name: "confirm",
  310. Type: "confirmation",
  311. }
  312. action := config.Action{Title: "Test"}
  313. err := typecheckActionArgument(&arg, "any_value", &action)
  314. assert.Nil(t, err, "Confirmation type should always pass validation")
  315. }
  316. func TestParseCommandForReplacements(t *testing.T) {
  317. tests := []struct {
  318. name string
  319. shellCommand string
  320. values map[string]string
  321. expectedOutput string
  322. expectError bool
  323. errorContains string
  324. }{
  325. {
  326. name: "Simple replacement",
  327. shellCommand: "echo {{ name }}",
  328. values: map[string]string{"name": "John"},
  329. expectedOutput: "echo John",
  330. expectError: false,
  331. },
  332. {
  333. name: "Multiple replacements",
  334. shellCommand: "echo {{ first }} {{ last }}",
  335. values: map[string]string{"first": "John", "last": "Doe"},
  336. expectedOutput: "echo John Doe",
  337. expectError: false,
  338. },
  339. {
  340. name: "Replacement with spaces in template",
  341. shellCommand: "echo {{ name }}",
  342. values: map[string]string{"name": "John"},
  343. expectedOutput: "echo John",
  344. expectError: false,
  345. },
  346. {
  347. name: "Missing argument",
  348. shellCommand: "echo {{ missing }}",
  349. values: map[string]string{},
  350. expectedOutput: "",
  351. expectError: true,
  352. errorContains: "required arg not provided: missing",
  353. },
  354. {
  355. name: "No replacements needed",
  356. shellCommand: "echo hello",
  357. values: map[string]string{},
  358. expectedOutput: "echo hello",
  359. expectError: false,
  360. },
  361. {
  362. name: "Multiple same argument",
  363. shellCommand: "echo {{ name }} says hello {{ name }}",
  364. values: map[string]string{"name": "Alice"},
  365. expectedOutput: "echo Alice says hello Alice",
  366. expectError: false,
  367. },
  368. }
  369. for _, tt := range tests {
  370. t.Run(tt.name, func(t *testing.T) {
  371. output, err := parseCommandForReplacements(tt.shellCommand, tt.values)
  372. if tt.expectError {
  373. assert.NotNil(t, err, "Expected error but got none")
  374. if tt.errorContains != "" {
  375. assert.Contains(t, err.Error(), tt.errorContains)
  376. }
  377. } else {
  378. assert.Nil(t, err, "Expected no error but got: %v", err)
  379. assert.Equal(t, tt.expectedOutput, output)
  380. }
  381. })
  382. }
  383. }
  384. func TestArgumentChoicesValidation(t *testing.T) {
  385. tests := []struct {
  386. name string
  387. action config.Action
  388. values map[string]string
  389. expectError bool
  390. description string
  391. }{
  392. {
  393. name: "Valid choice",
  394. action: config.Action{
  395. Title: "Test choices",
  396. Shell: "echo {{ option }}",
  397. Arguments: []config.ActionArgument{
  398. {
  399. Name: "option",
  400. Type: "ascii",
  401. Choices: []config.ActionArgumentChoice{
  402. {Value: "option1", Title: "Option 1"},
  403. {Value: "option2", Title: "Option 2"},
  404. },
  405. },
  406. },
  407. },
  408. values: map[string]string{"option": "option1"},
  409. expectError: false,
  410. description: "Should accept valid choice",
  411. },
  412. {
  413. name: "Invalid choice",
  414. action: config.Action{
  415. Title: "Test choices",
  416. Shell: "echo {{ option }}",
  417. Arguments: []config.ActionArgument{
  418. {
  419. Name: "option",
  420. Type: "ascii",
  421. Choices: []config.ActionArgumentChoice{
  422. {Value: "option1", Title: "Option 1"},
  423. {Value: "option2", Title: "Option 2"},
  424. },
  425. },
  426. },
  427. },
  428. values: map[string]string{"option": "invalid_option"},
  429. expectError: true,
  430. description: "Should reject invalid choice",
  431. },
  432. }
  433. for _, tt := range tests {
  434. t.Run(tt.name, func(t *testing.T) {
  435. _, err := parseActionArguments(tt.values, &tt.action, "")
  436. if tt.expectError {
  437. assert.NotNil(t, err, tt.description)
  438. assert.Contains(t, err.Error(), "predefined choices")
  439. } else {
  440. assert.Nil(t, err, tt.description)
  441. }
  442. })
  443. }
  444. }
  445. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  446. // This type should allow anything without validation
  447. tests := []string{
  448. "normal text",
  449. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  450. "$(rm -rf /)",
  451. "; DROP TABLE users; --",
  452. "../../../../etc/passwd",
  453. "",
  454. "unicode: 你好世界",
  455. "emojis: 🔥💀☠️",
  456. }
  457. for _, value := range tests {
  458. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  459. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  460. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  461. })
  462. }
  463. }
  464. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  465. action := config.Action{
  466. Title: "Test entity prefix",
  467. Shell: "echo 'Processing {{ name }} for entity'",
  468. Arguments: []config.ActionArgument{
  469. {Name: "name", Type: "ascii"},
  470. },
  471. }
  472. values := map[string]string{
  473. "name": "testuser",
  474. }
  475. // Test with entity prefix
  476. output, err := parseActionArguments(values, &action, "entity_123")
  477. assert.Nil(t, err)
  478. assert.Contains(t, output, "testuser")
  479. }
  480. func TestComplexRegexPatterns(t *testing.T) {
  481. tests := []struct {
  482. name string
  483. pattern string
  484. value string
  485. hasError bool
  486. }{
  487. {
  488. name: "Phone number pattern",
  489. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  490. value: "+1234567890",
  491. hasError: false,
  492. },
  493. {
  494. name: "Invalid phone number",
  495. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  496. value: "123abc",
  497. hasError: true,
  498. },
  499. {
  500. name: "Semantic version pattern",
  501. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  502. value: "1.2.3",
  503. hasError: false,
  504. },
  505. {
  506. name: "Invalid semantic version",
  507. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  508. value: "1.2",
  509. hasError: true,
  510. },
  511. }
  512. for _, tt := range tests {
  513. t.Run(tt.name, func(t *testing.T) {
  514. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  515. if tt.hasError {
  516. assert.NotNil(t, err)
  517. } else {
  518. assert.Nil(t, err)
  519. }
  520. })
  521. }
  522. }