4
0

arguments_test.go 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885
  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 TestCheckShellArgumentSafetyWithPassword(t *testing.T) {
  249. a1 := config.Action{
  250. Title: "Auth with password",
  251. Shell: "somecommand --password '{{password}}'",
  252. Arguments: []config.ActionArgument{
  253. {
  254. Name: "password",
  255. Type: "password",
  256. },
  257. },
  258. }
  259. err := checkShellArgumentSafety(&a1)
  260. assert.NotNil(t, err)
  261. assert.Contains(t, err.Error(), "unsafe argument type 'password' cannot be used with Shell execution")
  262. assert.Contains(t, err.Error(), "https://docs.olivetin.app/action_execution/shellvsexec.html")
  263. }
  264. func TestCheckShellArgumentSafetyWithPasswordAndExec(t *testing.T) {
  265. a1 := config.Action{
  266. Title: "Auth with password via exec",
  267. Exec: []string{"somecommand", "--password", "{{password}}"},
  268. Arguments: []config.ActionArgument{
  269. {
  270. Name: "password",
  271. Type: "password",
  272. },
  273. },
  274. }
  275. err := checkShellArgumentSafety(&a1)
  276. assert.Nil(t, err)
  277. }
  278. func TestTypeSafetyCheckUrl(t *testing.T) {
  279. assert.Nil(t, TypeSafetyCheck("test1", "http://google.com", "url"), "Test URL: google.com")
  280. assert.Nil(t, TypeSafetyCheck("test2", "http://technowax.net:80?foo=bar", "url"), "Test URL: technowax.net with query arguments")
  281. assert.Nil(t, TypeSafetyCheck("test3", "http://localhost:80?foo=bar", "url"), "Test URL: localhost with query arguments")
  282. assert.NotNil(t, TypeSafetyCheck("test4", "http://lo host:80", "url"), "Test a badly formed URL")
  283. assert.NotNil(t, TypeSafetyCheck("test5", "12345", "url"), "Test a badly formed URL")
  284. assert.NotNil(t, TypeSafetyCheck("test6", "_!23;", "url"), "Test a badly formed URL")
  285. }
  286. func TestTypeSafetyCheckRegex(t *testing.T) {
  287. tests := []struct {
  288. name string
  289. field string
  290. pattern string
  291. value string
  292. hasError bool
  293. }{
  294. {
  295. name: "Issue #578 - Domain",
  296. field: "domain",
  297. pattern: "regex:^(?:[a-zA-Z0-9-]{1,63}.)+[a-zA-Z]{2,63}$",
  298. value: "immich.example.dev",
  299. hasError: false,
  300. },
  301. {
  302. name: "Don't allow numbers in username",
  303. field: "Username",
  304. pattern: "regex:^[a-zA-Z]$",
  305. value: "James1234",
  306. hasError: true,
  307. },
  308. }
  309. for _, tt := range tests {
  310. t.Run(tt.name, func(t *testing.T) {
  311. err := typeSafetyCheckRegex(tt.field, tt.value, tt.pattern)
  312. if tt.hasError {
  313. assert.NotNil(t, err, "Expected error for value %s with pattern %s, but got no error", tt.value, tt.pattern)
  314. } else {
  315. assert.Nil(t, err, "Expected no error for value %s with pattern %s, but got error: %v", tt.value, tt.pattern, err)
  316. }
  317. })
  318. }
  319. }
  320. func TestRedactShellCommand(t *testing.T) {
  321. cmd := "echo 'The password for Fred is toomanysecrets'"
  322. args := []config.ActionArgument{
  323. {
  324. Name: "personName",
  325. Type: "ascii",
  326. },
  327. {
  328. Name: "password",
  329. Type: "password",
  330. },
  331. }
  332. values := map[string]string{
  333. "personName": "Fred",
  334. "password": "toomanysecrets",
  335. }
  336. res := redactShellCommand(cmd, args, values)
  337. assert.Equal(t, "echo 'The password for Fred is <redacted>'", res, "Redacted shell command should mask the password argument")
  338. // Test with empty password
  339. values["password"] = ""
  340. res = redactShellCommand(cmd, args, values)
  341. assert.Equal(t, cmd, res, "Empty password should not change the command")
  342. // Test with missing password argument
  343. delete(values, "password")
  344. res = redactShellCommand(cmd, args, values)
  345. assert.Equal(t, cmd, res, "Missing password argument should not change the command")
  346. }
  347. func TestTypeSafetyCheckEmail(t *testing.T) {
  348. tests := []struct {
  349. name string
  350. field string
  351. value string
  352. hasError bool
  353. }{
  354. {"Valid simple email", "email", "user@example.com", false},
  355. {"Valid email with subdomain", "email", "user@mail.example.com", false},
  356. {"Valid email with plus", "email", "user+test@example.com", false},
  357. {"Valid email with dash", "email", "user-name@example.com", false},
  358. {"Valid email with numbers", "email", "user123@example123.com", false},
  359. {"Invalid email no @", "email", "userexample.com", true},
  360. {"Invalid email no domain", "email", "user@", true},
  361. {"Invalid email no user", "email", "@example.com", true},
  362. {"Invalid email spaces", "email", "user name@example.com", true},
  363. {"Invalid email double @", "email", "user@@example.com", true},
  364. }
  365. for _, tt := range tests {
  366. t.Run(tt.name, func(t *testing.T) {
  367. err := TypeSafetyCheck(tt.field, tt.value, "email")
  368. if tt.hasError {
  369. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  370. } else {
  371. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  372. }
  373. })
  374. }
  375. }
  376. func TestTypeSafetyCheckDatetime(t *testing.T) {
  377. tests := []struct {
  378. name string
  379. field string
  380. value string
  381. hasError bool
  382. }{
  383. {"Valid datetime", "datetime", "2023-12-25T15:30:45", false},
  384. {"Valid datetime morning", "datetime", "2023-01-01T00:00:00", false},
  385. {"Valid datetime evening", "datetime", "2023-12-31T23:59:59", false},
  386. {"Invalid format missing T", "datetime", "2023-12-25 15:30:45", true},
  387. {"Invalid format missing seconds", "datetime", "2023-12-25T15:30", true},
  388. {"Invalid date", "datetime", "2023-13-25T15:30:45", true},
  389. {"Invalid time", "datetime", "2023-12-25T25:30:45", true},
  390. {"Random string", "datetime", "not-a-date", true},
  391. }
  392. for _, tt := range tests {
  393. t.Run(tt.name, func(t *testing.T) {
  394. err := TypeSafetyCheck(tt.field, tt.value, "datetime")
  395. if tt.hasError {
  396. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  397. } else {
  398. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  399. }
  400. })
  401. }
  402. }
  403. func TestTypeSafetyCheckRawStringMultiline(t *testing.T) {
  404. tests := []struct {
  405. name string
  406. field string
  407. value string
  408. }{
  409. {"Simple string", "content", "hello world"},
  410. {"Multiline string", "content", "line1\nline2\nline3"},
  411. {"String with special chars", "content", "!@#$%^&*()"},
  412. {"Unicode string", "content", "héllo wörld 🌍"},
  413. {"Very long string", "content", strings.Repeat("a", 1000)},
  414. }
  415. for _, tt := range tests {
  416. t.Run(tt.name, func(t *testing.T) {
  417. err := TypeSafetyCheck(tt.field, tt.value, "raw_string_multiline")
  418. assert.Nil(t, err, "raw_string_multiline should accept any value")
  419. })
  420. }
  421. }
  422. func TestTypeSafetyCheckUnicodeIdentifier(t *testing.T) {
  423. tests := []struct {
  424. name string
  425. field string
  426. value string
  427. expectsError bool
  428. }{
  429. {"Valid unicode identifier", "name", "hello_world", false},
  430. {"Valid with numbers", "name", "test123", false},
  431. {"Valid with dots", "name", "file.txt", false},
  432. {"Valid with underscores", "name", "my_file_name", false},
  433. {"Invalid with special chars", "name", "hello@world", true},
  434. {"Invalid with brackets", "name", "hello[world]", true},
  435. {"Invalid with spaces", "name", "hello world", true},
  436. {"Invalid with path separators", "name", "path/to/file", true},
  437. {"Invalid with backslashes", "name", "path\\to\\file", true},
  438. }
  439. for _, tt := range tests {
  440. t.Run(tt.name, func(t *testing.T) {
  441. err := TypeSafetyCheck(tt.field, tt.value, "unicode_identifier")
  442. validateTypeSafetyResult(t, tt.value, tt.expectsError, err)
  443. })
  444. }
  445. }
  446. func validateTypeSafetyResult(t *testing.T, value string, expectsError bool, err error) {
  447. if expectsError {
  448. assertErrorExpected(t, value, err)
  449. } else {
  450. assertNoErrorExpected(t, value, err)
  451. }
  452. }
  453. func assertErrorExpected(t *testing.T, value string, err error) {
  454. if err == nil {
  455. t.Errorf("Expected error for value '%s', but got none", value)
  456. } else {
  457. t.Logf("Received expected error for value '%s': %v", value, err)
  458. }
  459. }
  460. func assertNoErrorExpected(t *testing.T, value string, err error) {
  461. if err != nil {
  462. t.Errorf("Expected no error for value '%s', but got: %v", value, err)
  463. } else {
  464. t.Logf("No error for valid value '%s' as expected", value)
  465. }
  466. }
  467. func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) {
  468. tests := []struct {
  469. name string
  470. field string
  471. value string
  472. hasError bool
  473. }{
  474. {"Valid identifier", "name", "hello_world", false},
  475. {"Valid with numbers", "name", "test123", false},
  476. {"Valid with dots", "name", "file.txt", false},
  477. {"Valid with dashes", "name", "my-file", false},
  478. {"Valid with underscores", "name", "my_file", false},
  479. {"Invalid with spaces", "name", "hello world", true},
  480. {"Invalid with special chars", "name", "hello@world", true},
  481. {"Invalid unicode", "name", "héllo", true},
  482. }
  483. for _, tt := range tests {
  484. t.Run(tt.name, func(t *testing.T) {
  485. err := TypeSafetyCheck(tt.field, tt.value, "ascii_identifier")
  486. if tt.hasError {
  487. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  488. } else {
  489. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  490. }
  491. })
  492. }
  493. }
  494. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  495. tests := []struct {
  496. name string
  497. field string
  498. value string
  499. hasError bool
  500. }{
  501. {"Valid sentence", "text", "Hello world", false},
  502. {"Valid with numbers", "text", "Test 123", false},
  503. {"Valid with commas", "text", "Hello, world", false},
  504. {"Valid with periods", "text", "Hello world.", false},
  505. {"Valid with multiple spaces", "text", "Hello world", false},
  506. {"Invalid with special chars", "text", "Hello@world", true},
  507. {"Invalid with parentheses", "text", "Hello (world)", true},
  508. {"Invalid unicode", "text", "Héllo world", true},
  509. }
  510. for _, tt := range tests {
  511. t.Run(tt.name, func(t *testing.T) {
  512. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  513. if tt.hasError {
  514. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  515. } else {
  516. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  517. }
  518. })
  519. }
  520. }
  521. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  522. arg := config.ActionArgument{
  523. Name: "",
  524. Type: "ascii",
  525. }
  526. action := config.Action{Title: "Test"}
  527. err := typecheckActionArgument(&arg, "test", &action)
  528. assert.NotNil(t, err)
  529. assert.Contains(t, err.Error(), "argument name cannot be empty")
  530. }
  531. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  532. arg := config.ActionArgument{
  533. Name: "confirm",
  534. Type: "confirmation",
  535. }
  536. action := config.Action{Title: "Test"}
  537. err := typecheckActionArgument(&arg, "any_value", &action)
  538. assert.Nil(t, err, "Confirmation type should always pass validation")
  539. }
  540. func TestParseCommandForReplacements(t *testing.T) {
  541. tests := []struct {
  542. name string
  543. shellCommand string
  544. values map[string]string
  545. expectedOutput string
  546. expectError bool
  547. errorContains string
  548. }{
  549. {
  550. name: "Simple replacement",
  551. shellCommand: "echo {{ name }}",
  552. values: map[string]string{"name": "John"},
  553. expectedOutput: "echo John",
  554. expectError: false,
  555. },
  556. {
  557. name: "Multiple replacements",
  558. shellCommand: "echo {{ first }} {{ last }}",
  559. values: map[string]string{"first": "John", "last": "Doe"},
  560. expectedOutput: "echo John Doe",
  561. expectError: false,
  562. },
  563. {
  564. name: "Replacement with spaces in template",
  565. shellCommand: "echo {{ name }}",
  566. values: map[string]string{"name": "John"},
  567. expectedOutput: "echo John",
  568. expectError: false,
  569. },
  570. {
  571. name: "Missing argument",
  572. shellCommand: "echo {{ missing }}",
  573. values: map[string]string{},
  574. expectedOutput: "",
  575. expectError: true,
  576. errorContains: "required arg not provided: missing",
  577. },
  578. {
  579. name: "No replacements needed",
  580. shellCommand: "echo hello",
  581. values: map[string]string{},
  582. expectedOutput: "echo hello",
  583. expectError: false,
  584. },
  585. {
  586. name: "Multiple same argument",
  587. shellCommand: "echo {{ name }} says hello {{ name }}",
  588. values: map[string]string{"name": "Alice"},
  589. expectedOutput: "echo Alice says hello Alice",
  590. expectError: false,
  591. },
  592. }
  593. for _, tt := range tests {
  594. t.Run(tt.name, func(t *testing.T) {
  595. output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values)
  596. if tt.expectError {
  597. assert.NotNil(t, err, "Expected error but got none")
  598. if tt.errorContains != "" {
  599. assert.Contains(t, err.Error(), tt.errorContains)
  600. }
  601. } else {
  602. assert.Nil(t, err, "Expected no error but got: %v", err)
  603. assert.Equal(t, tt.expectedOutput, output)
  604. }
  605. })
  606. }
  607. }
  608. func TestArgumentChoicesValidation(t *testing.T) {
  609. tests := []struct {
  610. name string
  611. req *ExecutionRequest
  612. expectError bool
  613. description string
  614. }{
  615. {
  616. name: "Valid choice",
  617. req: &ExecutionRequest{
  618. Binding: &ActionBinding{
  619. Action: &config.Action{
  620. Title: "Test choices",
  621. Shell: "echo {{ option }}",
  622. Arguments: []config.ActionArgument{
  623. {
  624. Name: "option",
  625. Type: "ascii",
  626. Choices: []config.ActionArgumentChoice{
  627. {Value: "option1", Title: "Option 1"},
  628. {Value: "option2", Title: "Option 2"},
  629. },
  630. },
  631. },
  632. },
  633. },
  634. Arguments: map[string]string{"option": "option1"},
  635. },
  636. expectError: false,
  637. description: "Should accept valid choice",
  638. },
  639. {
  640. name: "Invalid choice",
  641. req: &ExecutionRequest{
  642. Binding: &ActionBinding{
  643. Action: &config.Action{
  644. Title: "Test choices",
  645. Shell: "echo {{ option }}",
  646. Arguments: []config.ActionArgument{
  647. {
  648. Name: "option",
  649. Type: "ascii",
  650. Choices: []config.ActionArgumentChoice{
  651. {Value: "option1", Title: "Option 1"},
  652. {Value: "option2", Title: "Option 2"},
  653. },
  654. },
  655. },
  656. },
  657. },
  658. Arguments: map[string]string{"option": "invalid_option"},
  659. },
  660. expectError: true,
  661. description: "Should reject invalid choice",
  662. },
  663. {
  664. name: "Invalid choice",
  665. req: &ExecutionRequest{
  666. Binding: &ActionBinding{
  667. Action: &config.Action{
  668. Title: "Test choices",
  669. Shell: "echo {{ option }}",
  670. Arguments: []config.ActionArgument{
  671. {
  672. Name: "option",
  673. Type: "ascii",
  674. Choices: []config.ActionArgumentChoice{
  675. {Value: "option1", Title: "Option 1"},
  676. {Value: "option2", Title: "Option 2"},
  677. },
  678. },
  679. },
  680. },
  681. },
  682. Arguments: map[string]string{"option": "option1"},
  683. },
  684. expectError: false,
  685. description: "Should accept valid choice",
  686. },
  687. }
  688. for _, tt := range tests {
  689. t.Run(tt.name, func(t *testing.T) {
  690. _, err := parseActionArguments(tt.req)
  691. if tt.expectError {
  692. assert.NotNil(t, err, tt.description)
  693. assert.Contains(t, err.Error(), "predefined choices")
  694. } else {
  695. assert.Nil(t, err, tt.description)
  696. }
  697. })
  698. }
  699. }
  700. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  701. // This type should allow anything without validation
  702. tests := []string{
  703. "normal text",
  704. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  705. "$(rm -rf /)",
  706. "; DROP TABLE users; --",
  707. "../../../../etc/passwd",
  708. "",
  709. "unicode: 你好世界",
  710. "emojis: 🔥💀☠️",
  711. }
  712. for _, value := range tests {
  713. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  714. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  715. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  716. })
  717. }
  718. }
  719. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  720. req := newExecRequest()
  721. req.Binding.Action = &config.Action{
  722. Title: "Test entity prefix",
  723. Shell: "echo 'Processing {{ name }} for entity'",
  724. Arguments: []config.ActionArgument{
  725. {Name: "name", Type: "ascii"},
  726. },
  727. }
  728. req.Arguments = map[string]string{
  729. "name": "testuser",
  730. }
  731. req.Binding.Entity = &entities.Entity{
  732. Title: "entity_123",
  733. }
  734. // Test with entity prefix
  735. output, err := parseActionArguments(req)
  736. assert.Nil(t, err)
  737. assert.Contains(t, output, "testuser")
  738. }
  739. func TestComplexRegexPatterns(t *testing.T) {
  740. tests := []struct {
  741. name string
  742. pattern string
  743. value string
  744. hasError bool
  745. }{
  746. {
  747. name: "Phone number pattern",
  748. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  749. value: "+1234567890",
  750. hasError: false,
  751. },
  752. {
  753. name: "Invalid phone number",
  754. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  755. value: "123abc",
  756. hasError: true,
  757. },
  758. {
  759. name: "Semantic version pattern",
  760. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  761. value: "1.2.3",
  762. hasError: false,
  763. },
  764. {
  765. name: "Invalid semantic version",
  766. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  767. value: "1.2",
  768. hasError: true,
  769. },
  770. }
  771. for _, tt := range tests {
  772. t.Run(tt.name, func(t *testing.T) {
  773. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  774. if tt.hasError {
  775. assert.NotNil(t, err)
  776. } else {
  777. assert.Nil(t, err)
  778. }
  779. })
  780. }
  781. }