arguments_test.go 16 KB

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