4
0

arguments_test.go 23 KB

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