executor_test.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490
  1. package executor
  2. import (
  3. "testing"
  4. "time"
  5. "github.com/stretchr/testify/assert"
  6. "github.com/OliveTin/OliveTin/internal/auth"
  7. authpublic "github.com/OliveTin/OliveTin/internal/auth/authpublic"
  8. config "github.com/OliveTin/OliveTin/internal/config"
  9. )
  10. func testingExecutor() (*Executor, *config.Config) {
  11. cfg := config.DefaultConfig()
  12. e := DefaultExecutor(cfg)
  13. a1 := &config.Action{
  14. Title: "Do some tickles",
  15. Shell: "echo 'Tickling {{ person }}'",
  16. Arguments: []config.ActionArgument{
  17. {
  18. Name: "person",
  19. Type: "ascii",
  20. },
  21. },
  22. }
  23. cfg.Actions = append(cfg.Actions, a1)
  24. cfg.Sanitize()
  25. return e, cfg
  26. }
  27. func TestCreateExecutorAndExec(t *testing.T) {
  28. e, cfg := testingExecutor()
  29. req := ExecutionRequest{
  30. AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "Mr Tickle"},
  31. Cfg: cfg,
  32. Arguments: map[string]string{
  33. "person": "yourself",
  34. },
  35. }
  36. // Ensure bindings are available and set the binding to the only configured action
  37. e.RebuildActionMap()
  38. if len(cfg.Actions) > 0 {
  39. req.Binding = e.FindBindingWithNoEntity(cfg.Actions[0])
  40. }
  41. assert.NotNil(t, e, "Create an executor")
  42. wg, _ := e.ExecRequest(&req)
  43. wg.Wait()
  44. assert.Equal(t, int32(0), req.logEntry.ExitCode, "Exit code is zero")
  45. }
  46. func TestExecNonExistant(t *testing.T) {
  47. e, cfg := testingExecutor()
  48. req := ExecutionRequest{
  49. // Binding: e.FindBindingWithNoEntity("waffles"),
  50. logEntry: &InternalLogEntry{},
  51. Cfg: cfg,
  52. }
  53. wg, _ := e.ExecRequest(&req)
  54. wg.Wait()
  55. assert.Equal(t, int32(-1337), req.logEntry.ExitCode, "Log entry is set to an internal error code")
  56. assert.Equal(t, "💩", req.logEntry.ActionIcon, "Log entry icon is a poop (not found)")
  57. }
  58. func TestArgumentNameCamelCase(t *testing.T) {
  59. req := newExecRequest()
  60. req.Binding.Action = &config.Action{
  61. Title: "Do some tickles",
  62. Shell: "echo 'Tickling {{ personName }}'",
  63. Arguments: []config.ActionArgument{
  64. {
  65. Name: "personName",
  66. Type: "ascii",
  67. },
  68. },
  69. }
  70. req.Arguments = map[string]string{
  71. "personName": "Fred",
  72. }
  73. out, err := parseActionArguments(req)
  74. assert.Equal(t, "echo 'Tickling Fred'", out)
  75. assert.Nil(t, err)
  76. }
  77. func TestArgumentNameSnakeCase(t *testing.T) {
  78. req := newExecRequest()
  79. req.Binding.Action = &config.Action{
  80. Title: "Do some tickles",
  81. Shell: "echo 'Tickling {{ person_name }}'",
  82. Arguments: []config.ActionArgument{
  83. {
  84. Name: "person_name",
  85. Type: "ascii",
  86. },
  87. },
  88. }
  89. req.Arguments = map[string]string{
  90. "person_name": "Fred",
  91. }
  92. out, err := parseActionArguments(req)
  93. assert.Equal(t, "echo 'Tickling Fred'", out)
  94. assert.Nil(t, err)
  95. }
  96. func TestGetLogsEmpty(t *testing.T) {
  97. e, cfg := testingExecutor()
  98. assert.Equal(t, int64(10), cfg.LogHistoryPageSize, "Logs page size should be 10")
  99. logs, paging := e.GetLogTrackingIds(0, 10)
  100. assert.NotNil(t, logs, "Logs should not be nil")
  101. assert.Equal(t, 0, len(logs), "No logs yet")
  102. assert.Equal(t, int64(0), paging.CountRemaining, "There should be no remaining logs")
  103. }
  104. func TestGetLogsLessThanPageSize(t *testing.T) {
  105. e, cfg := testingExecutor()
  106. cfg.Actions = append(cfg.Actions, &config.Action{
  107. Title: "blat",
  108. Shell: "date",
  109. })
  110. cfg.Sanitize()
  111. // Rebuild action map to include newly added action
  112. e.RebuildActionMap()
  113. assert.Equal(t, int64(10), cfg.LogHistoryPageSize, "Logs page size should be 10")
  114. logEntries, paging := e.GetLogTrackingIds(0, 10)
  115. assert.Equal(t, 0, len(logEntries), "There should be 0 logs")
  116. assert.Zero(t, paging.CountRemaining, "There should be no remaining logs")
  117. execNewReqAndWait(e, "blat", cfg)
  118. execNewReqAndWait(e, "blat", cfg)
  119. execNewReqAndWait(e, "blat", cfg)
  120. execNewReqAndWait(e, "blat", cfg)
  121. execNewReqAndWait(e, "blat", cfg)
  122. execNewReqAndWait(e, "blat", cfg)
  123. execNewReqAndWait(e, "blat", cfg)
  124. logEntries, paging = e.GetLogTrackingIds(0, 10)
  125. assert.Equal(t, 7, len(logEntries), "There should be 7 logs")
  126. assert.Zero(t, paging.CountRemaining, "There should be no remaining logs")
  127. execNewReqAndWait(e, "blat", cfg)
  128. execNewReqAndWait(e, "blat", cfg)
  129. execNewReqAndWait(e, "blat", cfg)
  130. execNewReqAndWait(e, "blat", cfg)
  131. execNewReqAndWait(e, "blat", cfg)
  132. logEntries, paging = e.GetLogTrackingIds(0, 10)
  133. assert.Equal(t, 10, len(logEntries), "There should be 10 logs")
  134. assert.Equal(t, int64(2), paging.CountRemaining, "There should be 1 remaining logs")
  135. }
  136. func execNewReqAndWait(e *Executor, title string, cfg *config.Config) {
  137. req := &ExecutionRequest{
  138. // ActionTitle: title,
  139. Cfg: cfg,
  140. }
  141. // Ensure we have a binding for the requested title
  142. e.RebuildActionMap()
  143. var action *config.Action
  144. for _, a := range cfg.Actions {
  145. if a.Title == title {
  146. action = a
  147. break
  148. }
  149. }
  150. if action != nil {
  151. req.Binding = e.FindBindingWithNoEntity(action)
  152. }
  153. wg, _ := e.ExecRequest(req)
  154. wg.Wait()
  155. }
  156. func TestGetPagingIndexes(t *testing.T) {
  157. assert.Zero(t, getPagingStartIndex(5, 0), "Testing start index from empty list")
  158. assert.Equal(t, int64(4), getPagingStartIndex(5, 10), "Testing start index from mid point")
  159. assert.Equal(t, int64(9), getPagingStartIndex(-1, 10), "Testing start index with negative offset")
  160. assert.Equal(t, int64(0), getPagingStartIndex(15, 10), "Testing start index with large offset")
  161. assert.Equal(t, int64(9), getPagingStartIndex(0, 10), "Testing start index with zero count")
  162. }
  163. func TestUnsetRequiredArgument(t *testing.T) {
  164. req := newExecRequest()
  165. req.Binding.Action = &config.Action{
  166. Title: "Print your name",
  167. Shell: "echo 'Your name is: {{ name }}'",
  168. Arguments: []config.ActionArgument{
  169. {
  170. Name: "name",
  171. Type: "ascii",
  172. },
  173. },
  174. }
  175. req.Arguments = map[string]string{}
  176. out, err := parseActionArguments(req)
  177. assert.Equal(t, "", out)
  178. assert.NotNil(t, err)
  179. }
  180. func TestUnusedArgumentStillPassesTypeSafetyCheck(t *testing.T) {
  181. req := newExecRequest()
  182. req.Binding.Action = &config.Action{
  183. Title: "Print your name",
  184. Shell: "echo 'Your name is: {{ name }}'",
  185. Arguments: []config.ActionArgument{
  186. {
  187. Name: "name",
  188. Type: "ascii",
  189. },
  190. {
  191. Name: "age",
  192. Type: "int",
  193. },
  194. },
  195. }
  196. req.Arguments = map[string]string{
  197. "name": "Fred",
  198. "age": "Not an integer",
  199. }
  200. out, err := parseActionArguments(req)
  201. assert.Equal(t, "", out)
  202. assert.NotNil(t, err)
  203. }
  204. // https://github.com/OliveTin/OliveTin/issues/564
  205. func TestMangleInvalidArgumentValues(t *testing.T) {
  206. e, cfg := testingExecutor()
  207. a1 := &config.Action{
  208. Title: "Validate my date without seconds because I am from an Android phone",
  209. Shell: "echo 'The date is: {{ date }}'",
  210. Arguments: []config.ActionArgument{
  211. {
  212. Name: "date",
  213. Type: "datetime",
  214. },
  215. },
  216. }
  217. cfg.Actions = append(cfg.Actions, a1)
  218. cfg.Sanitize()
  219. // Build bindings for newly added action
  220. e.RebuildActionMap()
  221. req := ExecutionRequest{
  222. // Action: a1,
  223. AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
  224. Cfg: cfg,
  225. Arguments: map[string]string{
  226. "date": "1990-01-10T12:00", // Invalid format, should be without seconds
  227. },
  228. }
  229. // Set binding to our appended action
  230. req.Binding = e.FindBindingWithNoEntity(a1)
  231. wg, _ := e.ExecRequest(&req)
  232. wg.Wait()
  233. assert.NotNil(t, req.logEntry, "Log entry should not be nil")
  234. assert.Equal(t, req.logEntry.Output, "The date is: 1990-01-10T12:00:00\n", "Date should be mangled to a valid format")
  235. }
  236. func TestWebhookRejectsShellExecution(t *testing.T) {
  237. cfg := config.DefaultConfig()
  238. e := DefaultExecutor(cfg)
  239. a1 := &config.Action{
  240. Title: "Webhook Shell Reject",
  241. Shell: "echo '{{ msg }}'",
  242. Arguments: []config.ActionArgument{
  243. {Name: "msg", Type: "ascii"},
  244. },
  245. }
  246. cfg.Actions = append(cfg.Actions, a1)
  247. cfg.Sanitize()
  248. e.RebuildActionMap()
  249. req := ExecutionRequest{
  250. Tags: []string{"webhook"},
  251. AuthenticatedUser: auth.UserFromSystem(cfg, "webhook"),
  252. Cfg: cfg,
  253. Arguments: map[string]string{"msg": "hello"},
  254. Binding: e.FindBindingWithNoEntity(a1),
  255. }
  256. wg, _ := e.ExecRequest(&req)
  257. wg.Wait()
  258. assert.NotNil(t, req.logEntry)
  259. assert.Equal(t, int32(-1337), req.logEntry.ExitCode)
  260. assert.Contains(t, req.logEntry.Output, "webhooks cannot use Shell execution")
  261. }
  262. func TestWebhookAllowsExecExecution(t *testing.T) {
  263. cfg := config.DefaultConfig()
  264. e := DefaultExecutor(cfg)
  265. a1 := &config.Action{
  266. Title: "Webhook Exec OK",
  267. Exec: []string{"echo", "{{ msg }}"},
  268. Arguments: []config.ActionArgument{
  269. {Name: "msg", Type: "ascii"},
  270. },
  271. }
  272. cfg.Actions = append(cfg.Actions, a1)
  273. cfg.Sanitize()
  274. e.RebuildActionMap()
  275. req := ExecutionRequest{
  276. Tags: []string{"webhook"},
  277. AuthenticatedUser: auth.UserFromSystem(cfg, "webhook"),
  278. Cfg: cfg,
  279. Arguments: map[string]string{"msg": "hello"},
  280. Binding: e.FindBindingWithNoEntity(a1),
  281. }
  282. wg, _ := e.ExecRequest(&req)
  283. wg.Wait()
  284. assert.NotNil(t, req.logEntry)
  285. assert.Equal(t, int32(0), req.logEntry.ExitCode)
  286. assert.Contains(t, req.logEntry.Output, "hello")
  287. }
  288. func TestFilterToDefinedArgumentsOnly(t *testing.T) {
  289. req := newExecRequest()
  290. req.Binding.Action = &config.Action{
  291. Title: "Filter test",
  292. Shell: "echo '{{ name }}'",
  293. Arguments: []config.ActionArgument{
  294. {Name: "name", Type: "ascii"},
  295. },
  296. }
  297. req.Arguments = map[string]string{
  298. "name": "Alice",
  299. "webhook_path": "/malicious/$(id)",
  300. "extra_undefined": "ignored",
  301. }
  302. filterToDefinedArgumentsOnly(req)
  303. assert.Equal(t, "Alice", req.Arguments["name"])
  304. assert.Empty(t, req.Arguments["webhook_path"])
  305. assert.Empty(t, req.Arguments["extra_undefined"])
  306. }
  307. func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) {
  308. req := newExecRequest()
  309. req.Binding.Action = &config.Action{
  310. Title: "Filter test",
  311. Shell: "echo test",
  312. Arguments: []config.ActionArgument{},
  313. }
  314. req.Arguments = map[string]string{
  315. "ot_executionTrackingId": "track-123",
  316. "ot_username": "webhook",
  317. }
  318. filterToDefinedArgumentsOnly(req)
  319. assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"])
  320. assert.Equal(t, "webhook", req.Arguments["ot_username"])
  321. }
  322. func TestTriggerExecutesTriggeredAction(t *testing.T) {
  323. cfg := config.DefaultConfig()
  324. e := DefaultExecutor(cfg)
  325. helloAction := &config.Action{
  326. Title: "Hello world",
  327. Shell: "echo 'Hello World!'",
  328. }
  329. triggerAction := &config.Action{
  330. Title: "Simple action that triggers another action",
  331. Shell: "echo 'Hi'",
  332. Triggers: []string{"Hello world"},
  333. }
  334. cfg.Actions = append(cfg.Actions, helloAction, triggerAction)
  335. cfg.Sanitize()
  336. e.RebuildActionMap()
  337. finishedTitles := make(chan string, 4)
  338. collector := &executionFinishedCollector{ch: finishedTitles}
  339. e.AddListener(collector)
  340. req := &ExecutionRequest{
  341. AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
  342. Cfg: cfg,
  343. Binding: e.FindBindingWithNoEntity(triggerAction),
  344. }
  345. wg, _ := e.ExecRequest(req)
  346. wg.Wait()
  347. var got []string
  348. for i := 0; i < 2; i++ {
  349. select {
  350. case title := <-finishedTitles:
  351. got = append(got, title)
  352. case <-time.After(2 * time.Second):
  353. t.Fatalf("timed out waiting for execution %d; got %v", i+1, got)
  354. }
  355. }
  356. assert.Contains(t, got, "Hello world", "triggered action must run")
  357. assert.Contains(t, got, "Simple action that triggers another action", "triggering action must run")
  358. }
  359. func TestTriggerUnknownActionTitleSkipsWithoutPanic(t *testing.T) {
  360. cfg := config.DefaultConfig()
  361. e := DefaultExecutor(cfg)
  362. triggerAction := &config.Action{
  363. Title: "Action with bad trigger",
  364. Shell: "echo 'ok'",
  365. Triggers: []string{"Nonexistent action"},
  366. }
  367. cfg.Actions = append(cfg.Actions, triggerAction)
  368. cfg.Sanitize()
  369. e.RebuildActionMap()
  370. finishedTitles := make(chan string, 4)
  371. collector := &executionFinishedCollector{ch: finishedTitles}
  372. e.AddListener(collector)
  373. req := &ExecutionRequest{
  374. AuthenticatedUser: auth.UserFromSystem(cfg, "testuser"),
  375. Cfg: cfg,
  376. Binding: e.FindBindingWithNoEntity(triggerAction),
  377. }
  378. wg, _ := e.ExecRequest(req)
  379. wg.Wait()
  380. var got []string
  381. select {
  382. case title := <-finishedTitles:
  383. got = append(got, title)
  384. case <-time.After(500 * time.Millisecond):
  385. }
  386. assert.Len(t, got, 1, "only the triggering action runs; unknown trigger is skipped")
  387. if len(got) > 0 {
  388. assert.Equal(t, "Action with bad trigger", got[0])
  389. }
  390. }
  391. type executionFinishedCollector struct {
  392. ch chan string
  393. }
  394. func (c *executionFinishedCollector) OnExecutionStarted(_ *InternalLogEntry) {}
  395. func (c *executionFinishedCollector) OnExecutionFinished(entry *InternalLogEntry) {
  396. c.ch <- entry.ActionTitle
  397. }
  398. func (c *executionFinishedCollector) OnOutputChunk(_ []byte, _ string) {}
  399. func (c *executionFinishedCollector) OnActionMapRebuilt() {}