arguments_test.go 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  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. "github.com/stretchr/testify/assert"
  8. "testing"
  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. hasError bool
  222. }{
  223. {"Valid unicode identifier", "name", "hello_world", false},
  224. {"Valid with numbers", "name", "test123", false},
  225. {"Valid with spaces", "name", "hello world", false},
  226. {"Valid with path separators", "name", "path/to/file", false},
  227. {"Valid with backslashes", "name", "path\\to\\file", false},
  228. {"Valid with dots", "name", "file.txt", false},
  229. {"Valid with underscores", "name", "my_file_name", false},
  230. {"Invalid with special chars", "name", "hello@world", true},
  231. {"Invalid with brackets", "name", "hello[world]", 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. if tt.hasError {
  237. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  238. } else {
  239. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  240. }
  241. })
  242. }
  243. }
  244. func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
  245. tests := []struct {
  246. name string
  247. field string
  248. value string
  249. hasError bool
  250. }{
  251. {"Valid identifier", "name", "hello_world", false},
  252. {"Valid with numbers", "name", "test123", false},
  253. {"Valid with dots", "name", "file.txt", false},
  254. {"Valid with dashes", "name", "my-file", false},
  255. {"Valid with underscores", "name", "my_file", false},
  256. {"Invalid with spaces", "name", "hello world", true},
  257. {"Invalid with special chars", "name", "hello@world", true},
  258. {"Invalid unicode", "name", "héllo", true},
  259. }
  260. for _, tt := range tests {
  261. t.Run(tt.name, func(t *testing.T) {
  262. err := TypeSafetyCheck(tt.field, tt.value, "ascii_identifier")
  263. if tt.hasError {
  264. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  265. } else {
  266. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  267. }
  268. })
  269. }
  270. }
  271. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  272. tests := []struct {
  273. name string
  274. field string
  275. value string
  276. hasError bool
  277. }{
  278. {"Valid sentence", "text", "Hello world", false},
  279. {"Valid with numbers", "text", "Test 123", false},
  280. {"Valid with commas", "text", "Hello, world", false},
  281. {"Valid with periods", "text", "Hello world.", false},
  282. {"Valid with multiple spaces", "text", "Hello world", false},
  283. {"Invalid with special chars", "text", "Hello@world", true},
  284. {"Invalid with parentheses", "text", "Hello (world)", true},
  285. {"Invalid unicode", "text", "Héllo world", true},
  286. }
  287. for _, tt := range tests {
  288. t.Run(tt.name, func(t *testing.T) {
  289. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  290. if tt.hasError {
  291. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  292. } else {
  293. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  294. }
  295. })
  296. }
  297. }
  298. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  299. arg := config.ActionArgument{
  300. Name: "",
  301. Type: "ascii",
  302. }
  303. action := config.Action{Title: "Test"}
  304. err := typecheckActionArgument(&arg, "test", &action)
  305. assert.NotNil(t, err)
  306. assert.Contains(t, err.Error(), "argument name cannot be empty")
  307. }
  308. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  309. arg := config.ActionArgument{
  310. Name: "confirm",
  311. Type: "confirmation",
  312. }
  313. action := config.Action{Title: "Test"}
  314. err := typecheckActionArgument(&arg, "any_value", &action)
  315. assert.Nil(t, err, "Confirmation type should always pass validation")
  316. }
  317. func TestParseCommandForReplacements(t *testing.T) {
  318. tests := []struct {
  319. name string
  320. shellCommand string
  321. values map[string]string
  322. expectedOutput string
  323. expectError bool
  324. errorContains string
  325. }{
  326. {
  327. name: "Simple replacement",
  328. shellCommand: "echo {{ name }}",
  329. values: map[string]string{"name": "John"},
  330. expectedOutput: "echo John",
  331. expectError: false,
  332. },
  333. {
  334. name: "Multiple replacements",
  335. shellCommand: "echo {{ first }} {{ last }}",
  336. values: map[string]string{"first": "John", "last": "Doe"},
  337. expectedOutput: "echo John Doe",
  338. expectError: false,
  339. },
  340. {
  341. name: "Replacement with spaces in template",
  342. shellCommand: "echo {{ name }}",
  343. values: map[string]string{"name": "John"},
  344. expectedOutput: "echo John",
  345. expectError: false,
  346. },
  347. {
  348. name: "Missing argument",
  349. shellCommand: "echo {{ missing }}",
  350. values: map[string]string{},
  351. expectedOutput: "",
  352. expectError: true,
  353. errorContains: "required arg not provided: missing",
  354. },
  355. {
  356. name: "No replacements needed",
  357. shellCommand: "echo hello",
  358. values: map[string]string{},
  359. expectedOutput: "echo hello",
  360. expectError: false,
  361. },
  362. {
  363. name: "Multiple same argument",
  364. shellCommand: "echo {{ name }} says hello {{ name }}",
  365. values: map[string]string{"name": "Alice"},
  366. expectedOutput: "echo Alice says hello Alice",
  367. expectError: false,
  368. },
  369. }
  370. for _, tt := range tests {
  371. t.Run(tt.name, func(t *testing.T) {
  372. output, err := parseCommandForReplacements(tt.shellCommand, tt.values, nil)
  373. if tt.expectError {
  374. assert.NotNil(t, err, "Expected error but got none")
  375. if tt.errorContains != "" {
  376. assert.Contains(t, err.Error(), tt.errorContains)
  377. }
  378. } else {
  379. assert.Nil(t, err, "Expected no error but got: %v", err)
  380. assert.Equal(t, tt.expectedOutput, output)
  381. }
  382. })
  383. }
  384. }
  385. func TestArgumentChoicesValidation(t *testing.T) {
  386. tests := []struct {
  387. name string
  388. action config.Action
  389. values map[string]string
  390. expectError bool
  391. description string
  392. }{
  393. {
  394. name: "Valid choice",
  395. action: config.Action{
  396. Title: "Test choices",
  397. Shell: "echo {{ option }}",
  398. Arguments: []config.ActionArgument{
  399. {
  400. Name: "option",
  401. Type: "ascii",
  402. Choices: []config.ActionArgumentChoice{
  403. {Value: "option1", Title: "Option 1"},
  404. {Value: "option2", Title: "Option 2"},
  405. },
  406. },
  407. },
  408. },
  409. values: map[string]string{"option": "option1"},
  410. expectError: false,
  411. description: "Should accept valid choice",
  412. },
  413. {
  414. name: "Invalid choice",
  415. action: config.Action{
  416. Title: "Test choices",
  417. Shell: "echo {{ option }}",
  418. Arguments: []config.ActionArgument{
  419. {
  420. Name: "option",
  421. Type: "ascii",
  422. Choices: []config.ActionArgumentChoice{
  423. {Value: "option1", Title: "Option 1"},
  424. {Value: "option2", Title: "Option 2"},
  425. },
  426. },
  427. },
  428. },
  429. values: map[string]string{"option": "invalid_option"},
  430. expectError: true,
  431. description: "Should reject invalid choice",
  432. },
  433. }
  434. for _, tt := range tests {
  435. t.Run(tt.name, func(t *testing.T) {
  436. _, err := parseActionArguments(tt.values, &tt.action, nil)
  437. if tt.expectError {
  438. assert.NotNil(t, err, tt.description)
  439. assert.Contains(t, err.Error(), "predefined choices")
  440. } else {
  441. assert.Nil(t, err, tt.description)
  442. }
  443. })
  444. }
  445. }
  446. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  447. // This type should allow anything without validation
  448. tests := []string{
  449. "normal text",
  450. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  451. "$(rm -rf /)",
  452. "; DROP TABLE users; --",
  453. "../../../../etc/passwd",
  454. "",
  455. "unicode: 你好世界",
  456. "emojis: 🔥💀☠️",
  457. }
  458. for _, value := range tests {
  459. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  460. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  461. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  462. })
  463. }
  464. }
  465. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  466. action := config.Action{
  467. Title: "Test entity prefix",
  468. Shell: "echo 'Processing {{ name }} for entity'",
  469. Arguments: []config.ActionArgument{
  470. {Name: "name", Type: "ascii"},
  471. },
  472. }
  473. values := map[string]string{
  474. "name": "testuser",
  475. }
  476. ent := &entities.Entity{
  477. Title: "entity_123",
  478. }
  479. // Test with entity prefix
  480. output, err := parseActionArguments(values, &action, ent)
  481. assert.Nil(t, err)
  482. assert.Contains(t, output, "testuser")
  483. }
  484. func TestComplexRegexPatterns(t *testing.T) {
  485. tests := []struct {
  486. name string
  487. pattern string
  488. value string
  489. hasError bool
  490. }{
  491. {
  492. name: "Phone number pattern",
  493. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  494. value: "+1234567890",
  495. hasError: false,
  496. },
  497. {
  498. name: "Invalid phone number",
  499. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  500. value: "123abc",
  501. hasError: true,
  502. },
  503. {
  504. name: "Semantic version pattern",
  505. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  506. value: "1.2.3",
  507. hasError: false,
  508. },
  509. {
  510. name: "Invalid semantic version",
  511. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  512. value: "1.2",
  513. hasError: true,
  514. },
  515. }
  516. for _, tt := range tests {
  517. t.Run(tt.name, func(t *testing.T) {
  518. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  519. if tt.hasError {
  520. assert.NotNil(t, err)
  521. } else {
  522. assert.Nil(t, err)
  523. }
  524. })
  525. }
  526. }