arguments_test.go 29 KB

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