4
0

arguments_test.go 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  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 TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) {
  495. tests := []struct {
  496. name string
  497. value string
  498. hasError bool
  499. }{
  500. {"Simple username", "alice123", false},
  501. {"Email username", "alice@example.com", false},
  502. {"Plus addressing", "alice+test@example.com", false},
  503. {"Hyphen underscore dot", "alice-test_user.example", false},
  504. {"Invalid space", "alice example", true},
  505. {"Invalid shell substitution", "$(whoami)", true},
  506. {"Invalid backtick", "`whoami`", true},
  507. {"Invalid semicolon", "alice;id", true},
  508. {"Invalid ampersand", "alice&id", true},
  509. {"Invalid pipe", "alice|id", true},
  510. {"Invalid quote", "alice'example", true},
  511. {"Invalid slash", "alice/example", true},
  512. }
  513. for _, tt := range tests {
  514. t.Run(tt.name, func(t *testing.T) {
  515. err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier")
  516. if tt.hasError {
  517. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  518. } else {
  519. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  520. }
  521. })
  522. }
  523. }
  524. func TestTypeSafetyCheckAsciiSentence(t *testing.T) {
  525. tests := []struct {
  526. name string
  527. field string
  528. value string
  529. hasError bool
  530. }{
  531. {"Valid sentence", "text", "Hello world", false},
  532. {"Valid with numbers", "text", "Test 123", false},
  533. {"Valid with commas", "text", "Hello, world", false},
  534. {"Valid with periods", "text", "Hello world.", false},
  535. {"Valid with multiple spaces", "text", "Hello world", false},
  536. {"Invalid with special chars", "text", "Hello@world", true},
  537. {"Invalid with parentheses", "text", "Hello (world)", true},
  538. {"Invalid unicode", "text", "Héllo world", true},
  539. }
  540. for _, tt := range tests {
  541. t.Run(tt.name, func(t *testing.T) {
  542. err := TypeSafetyCheck(tt.field, tt.value, "ascii_sentence")
  543. if tt.hasError {
  544. assert.NotNil(t, err, "Expected error for value '%s'", tt.value)
  545. } else {
  546. assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err)
  547. }
  548. })
  549. }
  550. }
  551. func TestTypecheckActionArgumentEmptyName(t *testing.T) {
  552. arg := config.ActionArgument{
  553. Name: "",
  554. Type: "ascii",
  555. }
  556. action := config.Action{Title: "Test"}
  557. err := typecheckActionArgument(&arg, "test", &action)
  558. assert.NotNil(t, err)
  559. assert.Contains(t, err.Error(), "argument name cannot be empty")
  560. }
  561. func TestTypecheckActionArgumentConfirmation(t *testing.T) {
  562. arg := config.ActionArgument{
  563. Name: "confirm",
  564. Type: "confirmation",
  565. }
  566. action := config.Action{Title: "Test"}
  567. err := typecheckActionArgument(&arg, "any_value", &action)
  568. assert.Nil(t, err, "Confirmation type should always pass validation")
  569. }
  570. func TestTypecheckActionArgumentHtmlWithoutName(t *testing.T) {
  571. action := config.Action{
  572. Title: "Delete old backups",
  573. Shell: "rm -rf /opt/oliveTinOldBackups/ && sleep 5",
  574. Arguments: []config.ActionArgument{
  575. {Type: "html", Title: "Description"},
  576. {Type: "confirmation", Title: "Are you sure?!"},
  577. },
  578. }
  579. err := validateArguments(map[string]string{}, &action)
  580. assert.NoError(t, err)
  581. }
  582. func TestParseCommandForReplacements(t *testing.T) {
  583. tests := []struct {
  584. name string
  585. shellCommand string
  586. values map[string]string
  587. expectedOutput string
  588. expectError bool
  589. errorContains string
  590. }{
  591. {
  592. name: "Simple replacement",
  593. shellCommand: "echo {{ name }}",
  594. values: map[string]string{"name": "John"},
  595. expectedOutput: "echo John",
  596. expectError: false,
  597. },
  598. {
  599. name: "Multiple replacements",
  600. shellCommand: "echo {{ first }} {{ last }}",
  601. values: map[string]string{"first": "John", "last": "Doe"},
  602. expectedOutput: "echo John Doe",
  603. expectError: false,
  604. },
  605. {
  606. name: "Replacement with spaces in template",
  607. shellCommand: "echo {{ name }}",
  608. values: map[string]string{"name": "John"},
  609. expectedOutput: "echo John",
  610. expectError: false,
  611. },
  612. {
  613. name: "Missing argument",
  614. shellCommand: "echo {{ missing }}",
  615. values: map[string]string{},
  616. expectedOutput: "",
  617. expectError: true,
  618. errorContains: "required arg not provided: missing",
  619. },
  620. {
  621. name: "No replacements needed",
  622. shellCommand: "echo hello",
  623. values: map[string]string{},
  624. expectedOutput: "echo hello",
  625. expectError: false,
  626. },
  627. {
  628. name: "Multiple same argument",
  629. shellCommand: "echo {{ name }} says hello {{ name }}",
  630. values: map[string]string{"name": "Alice"},
  631. expectedOutput: "echo Alice says hello Alice",
  632. expectError: false,
  633. },
  634. }
  635. for _, tt := range tests {
  636. t.Run(tt.name, func(t *testing.T) {
  637. output, err := tpl.ParseTemplateWithActionContext(tt.shellCommand, nil, tt.values)
  638. if tt.expectError {
  639. assert.NotNil(t, err, "Expected error but got none")
  640. if tt.errorContains != "" {
  641. assert.Contains(t, err.Error(), tt.errorContains)
  642. }
  643. } else {
  644. assert.Nil(t, err, "Expected no error but got: %v", err)
  645. assert.Equal(t, tt.expectedOutput, output)
  646. }
  647. })
  648. }
  649. }
  650. func TestArgumentChoicesValidation(t *testing.T) {
  651. tests := []struct {
  652. name string
  653. req *ExecutionRequest
  654. expectError bool
  655. description string
  656. }{
  657. {
  658. name: "Valid choice",
  659. req: &ExecutionRequest{
  660. Binding: &ActionBinding{
  661. Action: &config.Action{
  662. Title: "Test choices",
  663. Shell: "echo {{ option }}",
  664. Arguments: []config.ActionArgument{
  665. {
  666. Name: "option",
  667. Type: "ascii",
  668. Choices: []config.ActionArgumentChoice{
  669. {Value: "option1", Title: "Option 1"},
  670. {Value: "option2", Title: "Option 2"},
  671. },
  672. },
  673. },
  674. },
  675. },
  676. Arguments: map[string]string{"option": "option1"},
  677. },
  678. expectError: false,
  679. description: "Should accept valid choice",
  680. },
  681. {
  682. name: "Invalid choice",
  683. req: &ExecutionRequest{
  684. Binding: &ActionBinding{
  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": "invalid_option"},
  701. },
  702. expectError: true,
  703. description: "Should reject invalid choice",
  704. },
  705. {
  706. name: "Invalid choice",
  707. req: &ExecutionRequest{
  708. Binding: &ActionBinding{
  709. Action: &config.Action{
  710. Title: "Test choices",
  711. Shell: "echo {{ option }}",
  712. Arguments: []config.ActionArgument{
  713. {
  714. Name: "option",
  715. Type: "ascii",
  716. Choices: []config.ActionArgumentChoice{
  717. {Value: "option1", Title: "Option 1"},
  718. {Value: "option2", Title: "Option 2"},
  719. },
  720. },
  721. },
  722. },
  723. },
  724. Arguments: map[string]string{"option": "option1"},
  725. },
  726. expectError: false,
  727. description: "Should accept valid choice",
  728. },
  729. }
  730. for _, tt := range tests {
  731. t.Run(tt.name, func(t *testing.T) {
  732. _, err := parseActionArguments(tt.req)
  733. if tt.expectError {
  734. assert.NotNil(t, err, tt.description)
  735. assert.Contains(t, err.Error(), "predefined choices")
  736. } else {
  737. assert.Nil(t, err, tt.description)
  738. }
  739. })
  740. }
  741. }
  742. func TestTypeSafetyCheckVeryDangerousRawString(t *testing.T) {
  743. // This type should allow anything without validation
  744. tests := []string{
  745. "normal text",
  746. "_zomg_ c:/ haxxor ' bobby tables && rm -rf /",
  747. "$(rm -rf /)",
  748. "; DROP TABLE users; --",
  749. "../../../../etc/passwd",
  750. "",
  751. "unicode: 你好世界",
  752. "emojis: 🔥💀☠️",
  753. }
  754. for _, value := range tests {
  755. t.Run(fmt.Sprintf("Value: %s", value), func(t *testing.T) {
  756. err := TypeSafetyCheck("test", value, "very_dangerous_raw_string")
  757. assert.Nil(t, err, "very_dangerous_raw_string should accept any value including: %s", value)
  758. })
  759. }
  760. }
  761. func TestParseActionArgumentsWithEntityPrefix(t *testing.T) {
  762. req := newExecRequest()
  763. req.Binding.Action = &config.Action{
  764. Title: "Test entity prefix",
  765. Shell: "echo 'Processing {{ name }} for entity'",
  766. Arguments: []config.ActionArgument{
  767. {Name: "name", Type: "ascii"},
  768. },
  769. }
  770. req.Arguments = map[string]string{
  771. "name": "testuser",
  772. }
  773. req.Binding.Entity = &entities.Entity{
  774. Title: "entity_123",
  775. }
  776. // Test with entity prefix
  777. output, err := parseActionArguments(req)
  778. assert.Nil(t, err)
  779. assert.Contains(t, output, "testuser")
  780. }
  781. func TestComplexRegexPatterns(t *testing.T) {
  782. tests := []struct {
  783. name string
  784. pattern string
  785. value string
  786. hasError bool
  787. }{
  788. {
  789. name: "Phone number pattern",
  790. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  791. value: "+1234567890",
  792. hasError: false,
  793. },
  794. {
  795. name: "Invalid phone number",
  796. pattern: "regex:^\\+?[1-9]\\d{1,14}$",
  797. value: "123abc",
  798. hasError: true,
  799. },
  800. {
  801. name: "Semantic version pattern",
  802. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  803. value: "1.2.3",
  804. hasError: false,
  805. },
  806. {
  807. name: "Invalid semantic version",
  808. pattern: "regex:^(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)$",
  809. value: "1.2",
  810. hasError: true,
  811. },
  812. }
  813. for _, tt := range tests {
  814. t.Run(tt.name, func(t *testing.T) {
  815. err := typeSafetyCheckRegex("test", tt.value, tt.pattern)
  816. if tt.hasError {
  817. assert.NotNil(t, err)
  818. } else {
  819. assert.Nil(t, err)
  820. }
  821. })
  822. }
  823. }