arguments_test.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805
  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. log "github.com/sirupsen/logrus"
  8. "testing"
  9. "github.com/stretchr/testify/assert"
  10. )
  11. func TestSanitizeUnsafe(t *testing.T) {
  12. assert.Nil(t, TypeSafetyCheck("", "_zomg_ c:/ haxxor ' bobby tables && rm -rf ", "very_dangerous_raw_string"))
  13. }
  14. func TestSanitizeUnimplemented(t *testing.T) {
  15. err := TypeSafetyCheck("", "I am a happy little argument", "greeting_type")
  16. assert.NotNil(t, err, "Test an argument type that does not exist")
  17. }
  18. func TestValidateArgumentCheckboxDefaultValues(t *testing.T) {
  19. arg := config.ActionArgument{
  20. Name: "confirm",
  21. Type: "checkbox",
  22. }
  23. action := config.Action{
  24. Title: "Test checkbox default values",
  25. }
  26. // Default checkbox values without choices should accept "1" and "0"
  27. err := ValidateArgument(&arg, "1", &action)
  28. assert.Nil(t, err, "Expected checkbox value \"1\" to be accepted without choices")
  29. err = ValidateArgument(&arg, "0", &action)
  30. assert.Nil(t, err, "Expected checkbox value \"0\" to be accepted without choices")
  31. }
  32. func TestMangleCheckboxValueWithChoices(t *testing.T) {
  33. log.SetLevel(log.PanicLevel)
  34. arg := config.ActionArgument{
  35. Name: "confirm",
  36. Type: "checkbox",
  37. Choices: []config.ActionArgumentChoice{
  38. {Title: "Enabled", Value: "on"},
  39. {Title: "Disabled", Value: "off"},
  40. },
  41. }
  42. // When the incoming value matches a choice title, it should be mapped to the choice value
  43. out := mangleCheckboxValue(&arg, "Enabled", "Test action")
  44. assert.Equal(t, "on", out, "Expected checkbox title to be mangled to its value")
  45. out = mangleCheckboxValue(&arg, "Disabled", "Test action")
  46. assert.Equal(t, "off", out, "Expected checkbox title to be mangled to its value")
  47. // When there is no matching title, the value should be returned unchanged
  48. out = mangleCheckboxValue(&arg, "something-else", "Test action")
  49. assert.Equal(t, "something-else", out, "Expected non-matching value to be returned unchanged")
  50. }
  51. func TestMangleArgumentValueCheckbox(t *testing.T) {
  52. log.SetLevel(log.PanicLevel)
  53. arg := config.ActionArgument{
  54. Name: "confirm",
  55. Type: "checkbox",
  56. Choices: []config.ActionArgumentChoice{
  57. {Title: "Yes", Value: "true-value"},
  58. {Title: "No", Value: "false-value"},
  59. },
  60. }
  61. out := MangleArgumentValue(&arg, "Yes", "Test action")
  62. assert.Equal(t, "true-value", out, "Expected MangleArgumentValue to delegate to mangleCheckboxValue for checkbox types")
  63. out = MangleArgumentValue(&arg, "No", "Test action")
  64. assert.Equal(t, "false-value", out)
  65. // For non-matching values, it should return the original value
  66. out = MangleArgumentValue(&arg, "maybe", "Test action")
  67. assert.Equal(t, "maybe", out)
  68. }
  69. func TestValidateArgumentCheckboxWithChoices(t *testing.T) {
  70. log.SetLevel(log.PanicLevel)
  71. arg := config.ActionArgument{
  72. Name: "confirm",
  73. Type: "checkbox",
  74. Choices: []config.ActionArgumentChoice{
  75. {Title: "Enabled", Value: "on"},
  76. {Title: "Disabled", Value: "off"},
  77. },
  78. }
  79. action := config.Action{
  80. Title: "Test checkbox with choices",
  81. }
  82. // Titles should be accepted once mangled to their values
  83. err := ValidateArgument(&arg, "Enabled", &action)
  84. assert.Nil(t, err, "Expected checkbox title \"Enabled\" to be accepted after mangling to choice value")
  85. err = ValidateArgument(&arg, "Disabled", &action)
  86. assert.Nil(t, err, "Expected checkbox title \"Disabled\" to be accepted after mangling to choice value")
  87. // Unknown titles should be rejected because they do not match any choice value
  88. err = ValidateArgument(&arg, "Maybe", &action)
  89. assert.NotNil(t, err, "Expected unknown checkbox title to be rejected against choices")
  90. }
  91. func TestArgumentValueNullable(t *testing.T) {
  92. a1 := config.Action{
  93. Title: "Release the hounds",
  94. Shell: "echo 'Releasing {{ count }} hounds'",
  95. Arguments: []config.ActionArgument{
  96. {
  97. Name: "count",
  98. Type: "int",
  99. },
  100. },
  101. }
  102. values := map[string]string{
  103. "count": "",
  104. }
  105. out, err := parseActionArguments(values, &a1, nil)
  106. assert.Equal(t, "echo 'Releasing hounds'", out)
  107. assert.Nil(t, err)
  108. a1.Arguments[0].RejectNull = true
  109. _, err = parseActionArguments(values, &a1, nil)
  110. assert.NotNil(t, err)
  111. }
  112. func TestArgumentNameNumbers(t *testing.T) {
  113. a1 := config.Action{
  114. Title: "Do some tickles",
  115. Shell: "echo 'Tickling {{ person1name }}'",
  116. Arguments: []config.ActionArgument{
  117. {
  118. Name: "person1name",
  119. Type: "ascii",
  120. },
  121. },
  122. }
  123. values := map[string]string{
  124. "person1name": "Fred",
  125. }
  126. out, err := parseActionArguments(values, &a1, nil)
  127. assert.Equal(t, "echo 'Tickling Fred'", out)
  128. assert.Nil(t, err)
  129. }
  130. func TestArgumentNotProvided(t *testing.T) {
  131. a1 := config.Action{
  132. Title: "Do some tickles",
  133. Shell: "echo 'Tickling {{ personName }}'",
  134. Arguments: []config.ActionArgument{
  135. {
  136. Name: "person",
  137. Type: "ascii",
  138. },
  139. },
  140. }
  141. values := map[string]string{}
  142. out, err := parseActionArguments(values, &a1, nil)
  143. assert.Equal(t, "", out)
  144. assert.Equal(t, err.Error(), "required arg not provided: personName")
  145. }
  146. func TestExecArrayParsing(t *testing.T) {
  147. a1 := config.Action{
  148. Title: "List files",
  149. Exec: []string{"ls", "-alh"},
  150. Arguments: []config.ActionArgument{},
  151. }
  152. values := map[string]string{}
  153. out, err := parseActionExec(values, &a1, nil)
  154. assert.Nil(t, err)
  155. assert.Equal(t, []string{"ls", "-alh"}, out)
  156. }
  157. func TestExecArrayWithTemplateReplacement(t *testing.T) {
  158. a1 := config.Action{
  159. Title: "List specific path",
  160. Exec: []string{"ls", "-alh", "{{path}}"},
  161. Arguments: []config.ActionArgument{
  162. {
  163. Name: "path",
  164. Type: "ascii_identifier",
  165. },
  166. },
  167. }
  168. values := map[string]string{
  169. "path": "tmp",
  170. }
  171. out, err := parseActionExec(values, &a1, nil)
  172. assert.Nil(t, err)
  173. assert.Equal(t, []string{"ls", "-alh", "tmp"}, out)
  174. }
  175. func TestCheckShellArgumentSafetyWithURL(t *testing.T) {
  176. a1 := config.Action{
  177. Title: "Download file",
  178. Shell: "curl {{url}}",
  179. Arguments: []config.ActionArgument{
  180. {
  181. Name: "url",
  182. Type: "url",
  183. },
  184. },
  185. }
  186. err := checkShellArgumentSafety(&a1)
  187. assert.NotNil(t, err)
  188. assert.Contains(t, err.Error(), "unsafe argument type 'url' cannot be used with Shell execution")
  189. assert.Contains(t, err.Error(), "https://docs.olivetin.app/action_execution/shellvsexec.html")
  190. }
  191. func TestCheckShellArgumentSafetyWithEmail(t *testing.T) {
  192. a1 := config.Action{
  193. Title: "Send email",
  194. Shell: "sendmail {{email}}",
  195. Arguments: []config.ActionArgument{
  196. {
  197. Name: "email",
  198. Type: "email",
  199. },
  200. },
  201. }
  202. err := checkShellArgumentSafety(&a1)
  203. assert.NotNil(t, err)
  204. assert.Contains(t, err.Error(), "unsafe argument type 'email' cannot be used with Shell execution")
  205. }
  206. func TestCheckShellArgumentSafetyWithExec(t *testing.T) {
  207. a1 := config.Action{
  208. Title: "Download file",
  209. Exec: []string{"curl", "{{url}}"},
  210. Arguments: []config.ActionArgument{
  211. {
  212. Name: "url",
  213. Type: "url",
  214. },
  215. },
  216. }
  217. err := checkShellArgumentSafety(&a1)
  218. assert.Nil(t, err)
  219. }
  220. func TestCheckShellArgumentSafetyWithSafeTypes(t *testing.T) {
  221. a1 := config.Action{
  222. Title: "List files",
  223. Shell: "ls {{path}}",
  224. Arguments: []config.ActionArgument{
  225. {
  226. Name: "path",
  227. Type: "ascii_identifier",
  228. },
  229. },
  230. }
  231. err := checkShellArgumentSafety(&a1)
  232. assert.Nil(t, err)
  233. }
  234. func TestTypeSafetyCheckUrl(t *testing.T) {
  235. assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
  236. assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
  237. assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
  238. assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
  239. assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
  240. assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
  241. }
  242. func TestTypeSafetyCheckRegex(t *testing.T) {
  243. tests := []struct {
  244. name string
  245. field string
  246. pattern string
  247. value string
  248. hasError bool
  249. }{
  250. {
  251. name: "Issue #578 - Domain",
  252. field: "domain",
  253. pattern: "regex:^(?:[a-zA-Z0-9-]{1,63}.)+[a-zA-Z]{2,63}$",
  254. value: "immich.example.dev",
  255. hasError: false,
  256. },
  257. {
  258. name: "Don't allow numbers in username",
  259. field: "Username",
  260. pattern: "regex:^[a-zA-Z]$",
  261. value: "James1234",
  262. hasError: true,
  263. },
  264. }
  265. for _, tt := range tests {
  266. t.Run(tt.name, func(t *testing.T) {
  267. err := typeSafetyCheckRegex(tt.field, tt.value, tt.pattern)
  268. if tt.hasError {
  269. assert.NotNil(t, err, "Expected error for value %s with pattern %s, but got no error", tt.value, tt.pattern)
  270. } else {
  271. assert.Nil(t, err, "Expected no error for value %s with pattern %s, but got error: %v", tt.value, tt.pattern, err)
  272. }
  273. })
  274. }
  275. }
  276. func TestRedactShellCommand(t *testing.T) {
  277. cmd := "echo 'The password for Fred is toomanysecrets'"
  278. args := []config.ActionArgument{
  279. {
  280. Name: "personName",
  281. Type: "ascii",
  282. },
  283. {
  284. Name: "password",
  285. Type: "password",
  286. },
  287. }
  288. values := map[string]string{
  289. "personName": "Fred",
  290. "password": "toomanysecrets",
  291. }
  292. res := redactShellCommand(cmd, args, values)
  293. assert.Equal(t, "echo 'The password for Fred is <redacted>'", res, "Redacted shell command should mask the password argument")
  294. // Test with empty password
  295. values["password"] = ""
  296. res = redactShellCommand(cmd, args, values)
  297. assert.Equal(t, cmd, res, "Empty password should not change the command")
  298. // Test with missing password argument
  299. delete(values, "password")
  300. res = redactShellCommand(cmd, args, values)
  301. assert.Equal(t, cmd, res, "Missing password argument should not change the command")
  302. }
  303. func TestTypeSafetyCheckEmail(t *testing.T) {
  304. tests := []struct {
  305. name string
  306. field string
  307. value string
  308. hasError bool
  309. }{
  310. {"Valid simple email", "email", "user@example.com", false},
  311. {"Valid email with subdomain", "email", "user@mail.example.com", false},
  312. {"Valid email with plus", "email", "user+test@example.com", false},
  313. {"Valid email with dash", "email", "user-name@example.com", false},
  314. {"Valid email with numbers", "email", "user123@example123.com", false},
  315. {"Invalid email no @", "email", "userexample.com", true},
  316. {"Invalid email no domain", "email", "user@", true},
  317. {"Invalid email no user", "email", "@example.com", true},
  318. {"Invalid email spaces", "email", "user name@example.com", true},
  319. {"Invalid email double @", "email", "user@@example.com", true},
  320. }
  321. for _, tt := range tests {
  322. t.Run(tt.name, func(t *testing.T) {
  323. err := TypeSafetyCheck(tt.field, tt.value, "email")
  324. if tt.hasError {
  325. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  326. } else {
  327. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  328. }
  329. })
  330. }
  331. }
  332. func TestTypeSafetyCheckDatetime(t *testing.T) {
  333. tests := []struct {
  334. name string
  335. field string
  336. value string
  337. hasError bool
  338. }{
  339. {"Valid datetime", "datetime", "2023-12-25T15:30:45", false},
  340. {"Valid datetime morning", "datetime", "2023-01-01T00:00:00", false},
  341. {"Valid datetime evening", "datetime", "2023-12-31T23:59:59", false},
  342. {"Invalid format missing T", "datetime", "2023-12-25 15:30:45", true},
  343. {"Invalid format missing seconds", "datetime", "2023-12-25T15:30", true},
  344. {"Invalid date", "datetime", "2023-13-25T15:30:45", true},
  345. {"Invalid time", "datetime", "2023-12-25T25:30:45", true},
  346. {"Random string", "datetime", "not-a-date", true},
  347. }
  348. for _, tt := range tests {
  349. t.Run(tt.name, func(t *testing.T) {
  350. err := TypeSafetyCheck(tt.field, tt.value, "datetime")
  351. if tt.hasError {
  352. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  353. } else {
  354. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  355. }
  356. })
  357. }
  358. }
  359. func TestTypeSafetyCheckRawStringMultiline(t *testing.T) {
  360. tests := []struct {
  361. name string
  362. field string
  363. value string
  364. }{
  365. {"Simple string", "content", "hello world"},
  366. {"Multiline string", "content", "line1\nline2\nline3"},
  367. {"String with special chars", "content", "!@#$%^&*()"},
  368. {"Unicode string", "content", "héllo wörld 🌍"},
  369. {"Very long string", "content", strings.Repeat("a", 1000)},
  370. }
  371. for _, tt := range tests {
  372. t.Run(tt.name, func(t *testing.T) {
  373. err := TypeSafetyCheck(tt.field, tt.value, "raw_string_multiline")
  374. assert.Nil(t, err, "raw_string_multiline should accept any value")
  375. })
  376. }
  377. }
  378. func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) {
  379. tests := []struct {
  380. name string
  381. field string
  382. value string
  383. expectsError bool
  384. }{
  385. {"Valid unicode identifier", "name", "hello_world", false},
  386. {"Valid with numbers", "name", "test123", false},
  387. {"Valid with dots", "name", "file.txt", false},
  388. {"Valid with underscores", "name", "my_file_name", false},
  389. {"Invalid with special chars", "name", "hello@world", true},
  390. {"Invalid with brackets", "name", "hello[world]", true},
  391. {"Invalid with spaces", "name", "hello world", true},
  392. {"Invalid with path separators", "name", "path/to/file", true},
  393. {"Invalid with backslashes", "name", "path\\to\\file", true},
  394. }
  395. for _, tt := range tests {
  396. t.Run(tt.name, func(t *testing.T) {
  397. err := TypeSafetyCheck(tt.field, tt.value, "unicode_identifier")
  398. validateTypeSafetyResult(t, tt.value, tt.expectsError, err)
  399. })
  400. }
  401. }
  402. func validateTypeSafetyResult(t *testing.T, value string, expectsError bool, err error) {
  403. if expectsError {
  404. assertErrorExpected(t, value, err)
  405. } else {
  406. assertNoErrorExpected(t, value, err)
  407. }
  408. }
  409. func assertErrorExpected(t *testing.T, value string, err error) {
  410. if err == nil {
  411. t.Errorf("Expected error for value '%s', but got none", value)
  412. } else {
  413. t.Logf("Received expected error for value '%s': %v", value, err)
  414. }
  415. }
  416. func assertNoErrorExpected(t *testing.T, value string, err error) {
  417. if err != nil {
  418. t.Errorf("Expected no error for value '%s', but got: %v", value, err)
  419. } else {
  420. t.Logf("No error for valid value '%s' as expected", value)
  421. }
  422. }
  423. func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
  424. tests := []struct {
  425. name string
  426. field string
  427. value string
  428. hasError bool
  429. }{
  430. {"Valid identifier", "name", "hello_world", false},
  431. {"Valid with numbers", "name", "test123", false},
  432. {"Valid with dots", "name", "file.txt", false},
  433. {"Valid with dashes", "name", "my-file", false},
  434. {"Valid with underscores", "name", "my_file", false},
  435. {"Invalid with spaces", "name", "hello world", true},
  436. {"Invalid with special chars", "name", "hello@world", true},
  437. {"Invalid unicode", "name", "héllo", true},
  438. }
  439. for _, tt := range tests {
  440. t.Run(tt.name, func(t *testing.T) {
  441. err := TypeSafetyCheck(tt.field, tt.value, "ascii_identifier")
  442. if tt.hasError {
  443. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  444. } else {
  445. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  446. }
  447. })
  448. }
  449. }
  450. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  451. tests := []struct {
  452. name string
  453. field string
  454. value string
  455. hasError bool
  456. }{
  457. {"Valid sentence", "text", "Hello world", false},
  458. {"Valid with numbers", "text", "Test 123", false},
  459. {"Valid with commas", "text", "Hello, world", false},
  460. {"Valid with periods", "text", "Hello world.", false},
  461. {"Valid with multiple spaces", "text", "Hello world", false},
  462. {"Invalid with special chars", "text", "Hello@world", true},
  463. {"Invalid with parentheses", "text", "Hello (world)", true},
  464. {"Invalid unicode", "text", "Héllo world", true},
  465. }
  466. for _, tt := range tests {
  467. t.Run(tt.name, func(t *testing.T) {
  468. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  469. if tt.hasError {
  470. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  471. } else {
  472. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  473. }
  474. })
  475. }
  476. }
  477. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  478. arg := config.ActionArgument{
  479. Name: "",
  480. Type: "ascii",
  481. }
  482. action := config.Action{Title: "Test"}
  483. err := typecheckActionArgument(&arg, "test", &action)
  484. assert.NotNil(t, err)
  485. assert.Contains(t, err.Error(), "argument name cannot be empty")
  486. }
  487. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  488. arg := config.ActionArgument{
  489. Name: "confirm",
  490. Type: "confirmation",
  491. }
  492. action := config.Action{Title: "Test"}
  493. err := typecheckActionArgument(&arg, "any_value", &action)
  494. assert.Nil(t, err, "Confirmation type should always pass validation")
  495. }
  496. func TestParseCommandForReplacements(t *testing.T) {
  497. tests := []struct {
  498. name string
  499. shellCommand string
  500. values map[string]string
  501. expectedOutput string
  502. expectError bool
  503. errorContains string
  504. }{
  505. {
  506. name: "Simple replacement",
  507. shellCommand: "echo {{ name }}",
  508. values: map[string]string{"name": "John"},
  509. expectedOutput: "echo John",
  510. expectError: false,
  511. },
  512. {
  513. name: "Multiple replacements",
  514. shellCommand: "echo {{ first }} {{ last }}",
  515. values: map[string]string{"first": "John", "last": "Doe"},
  516. expectedOutput: "echo John Doe",
  517. expectError: false,
  518. },
  519. {
  520. name: "Replacement with spaces in template",
  521. shellCommand: "echo {{ name }}",
  522. values: map[string]string{"name": "John"},
  523. expectedOutput: "echo John",
  524. expectError: false,
  525. },
  526. {
  527. name: "Missing argument",
  528. shellCommand: "echo {{ missing }}",
  529. values: map[string]string{},
  530. expectedOutput: "",
  531. expectError: true,
  532. errorContains: "required arg not provided: missing",
  533. },
  534. {
  535. name: "No replacements needed",
  536. shellCommand: "echo hello",
  537. values: map[string]string{},
  538. expectedOutput: "echo hello",
  539. expectError: false,
  540. },
  541. {
  542. name: "Multiple same argument",
  543. shellCommand: "echo {{ name }} says hello {{ name }}",
  544. values: map[string]string{"name": "Alice"},
  545. expectedOutput: "echo Alice says hello Alice",
  546. expectError: false,
  547. },
  548. }
  549. for _, tt := range tests {
  550. t.Run(tt.name, func(t *testing.T) {
  551. output, err := parseCommandForReplacements(tt.shellCommand, tt.values, nil)
  552. if tt.expectError {
  553. assert.NotNil(t, err, "Expected error but got none")
  554. if tt.errorContains != "" {
  555. assert.Contains(t, err.Error(), tt.errorContains)
  556. }
  557. } else {
  558. assert.Nil(t, err, "Expected no error but got: %v", err)
  559. assert.Equal(t, tt.expectedOutput, output)
  560. }
  561. })
  562. }
  563. }
  564. func TestArgumentChoicesValidation(t *testing.T) {
  565. tests := []struct {
  566. name string
  567. action config.Action
  568. values map[string]string
  569. expectError bool
  570. description string
  571. }{
  572. {
  573. name: "Valid choice",
  574. action: config.Action{
  575. Title: "Test choices",
  576. Shell: "echo {{ option }}",
  577. Arguments: []config.ActionArgument{
  578. {
  579. Name: "option",
  580. Type: "ascii",
  581. Choices: []config.ActionArgumentChoice{
  582. {Value: "option1", Title: "Option 1"},
  583. {Value: "option2", Title: "Option 2"},
  584. },
  585. },
  586. },
  587. },
  588. values: map[string]string{"option": "option1"},
  589. expectError: false,
  590. description: "Should accept valid choice",
  591. },
  592. {
  593. name: "Invalid choice",
  594. action: config.Action{
  595. Title: "Test choices",
  596. Shell: "echo {{ option }}",
  597. Arguments: []config.ActionArgument{
  598. {
  599. Name: "option",
  600. Type: "ascii",
  601. Choices: []config.ActionArgumentChoice{
  602. {Value: "option1", Title: "Option 1"},
  603. {Value: "option2", Title: "Option 2"},
  604. },
  605. },
  606. },
  607. },
  608. values: map[string]string{"option": "invalid_option"},
  609. expectError: true,
  610. description: "Should reject invalid choice",
  611. },
  612. }
  613. for _, tt := range tests {
  614. t.Run(tt.name, func(t *testing.T) {
  615. _, err := parseActionArguments(tt.values, &tt.action, nil)
  616. if tt.expectError {
  617. assert.NotNil(t, err, tt.description)
  618. assert.Contains(t, err.Error(), "predefined choices")
  619. } else {
  620. assert.Nil(t, err, tt.description)
  621. }
  622. })
  623. }
  624. }
  625. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  626. // This type should allow anything without validation
  627. tests := []string{
  628. "normal text",
  629. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  630. "$(rm -rf /)",
  631. "; DROP TABLE users; --",
  632. "../../../../etc/passwd",
  633. "",
  634. "unicode: 你好世界",
  635. "emojis: 🔥💀☠️",
  636. }
  637. for _, value := range tests {
  638. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  639. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  640. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  641. })
  642. }
  643. }
  644. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  645. action := config.Action{
  646. Title: "Test entity prefix",
  647. Shell: "echo 'Processing {{ name }} for entity'",
  648. Arguments: []config.ActionArgument{
  649. {Name: "name", Type: "ascii"},
  650. },
  651. }
  652. values := map[string]string{
  653. "name": "testuser",
  654. }
  655. ent := &entities.Entity{
  656. Title: "entity_123",
  657. }
  658. // Test with entity prefix
  659. output, err := parseActionArguments(values, &action, ent)
  660. assert.Nil(t, err)
  661. assert.Contains(t, output, "testuser")
  662. }
  663. func TestComplexRegexPatterns(t *testing.T) {
  664. tests := []struct {
  665. name string
  666. pattern string
  667. value string
  668. hasError bool
  669. }{
  670. {
  671. name: "Phone number pattern",
  672. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  673. value: "+1234567890",
  674. hasError: false,
  675. },
  676. {
  677. name: "Invalid phone number",
  678. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  679. value: "123abc",
  680. hasError: true,
  681. },
  682. {
  683. name: "Semantic version pattern",
  684. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  685. value: "1.2.3",
  686. hasError: false,
  687. },
  688. {
  689. name: "Invalid semantic version",
  690. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  691. value: "1.2",
  692. hasError: true,
  693. },
  694. }
  695. for _, tt := range tests {
  696. t.Run(tt.name, func(t *testing.T) {
  697. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  698. if tt.hasError {
  699. assert.NotNil(t, err)
  700. } else {
  701. assert.Nil(t, err)
  702. }
  703. })
  704. }
  705. }