4
0

arguments_test.go 24 KB

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