trigger_agent_spec.rb 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. require 'rails_helper'
  2. describe Agents::TriggerAgent do
  3. before do
  4. @valid_params = {
  5. 'name' => "my trigger agent",
  6. 'options' => {
  7. 'expected_receive_period_in_days' => 2,
  8. 'rules' => [{
  9. 'type' => "regex",
  10. 'value' => "a\\db",
  11. 'path' => "foo.bar.baz",
  12. }],
  13. 'message' => "I saw '{{foo.bar.baz}}' from {{name}}"
  14. }
  15. }
  16. @checker = Agents::TriggerAgent.new(@valid_params)
  17. @checker.user = users(:bob)
  18. @checker.save!
  19. @event = Event.new
  20. @event.agent = agents(:bob_rain_notifier_agent)
  21. @event.payload = { 'foo' => { "bar" => { 'baz' => "a2b" } },
  22. 'name' => "Joe" }
  23. end
  24. describe "validation" do
  25. before do
  26. expect(@checker).to be_valid
  27. end
  28. it "should validate presence of message" do
  29. @checker.options['message'] = nil
  30. expect(@checker).not_to be_valid
  31. @checker.options['message'] = ''
  32. expect(@checker).not_to be_valid
  33. end
  34. it "should be valid without a message when 'keep_event' is set" do
  35. @checker.options['keep_event'] = 'true'
  36. @checker.options['message'] = ''
  37. expect(@checker).to be_valid
  38. end
  39. it "if present, 'keep_event' must equal true or false" do
  40. @checker.options['keep_event'] = 'true'
  41. expect(@checker).to be_valid
  42. @checker.options['keep_event'] = 'false'
  43. expect(@checker).to be_valid
  44. @checker.options['keep_event'] = ''
  45. expect(@checker).to be_valid
  46. @checker.options['keep_event'] = 'tralse'
  47. expect(@checker).not_to be_valid
  48. end
  49. it "validates that 'must_match' is a positive integer, not greater than the number of rules, if provided" do
  50. @checker.options['must_match'] = '1'
  51. expect(@checker).to be_valid
  52. @checker.options['must_match'] = '0'
  53. expect(@checker).not_to be_valid
  54. @checker.options['must_match'] = 'wrong'
  55. expect(@checker).not_to be_valid
  56. @checker.options['must_match'] = ''
  57. expect(@checker).to be_valid
  58. @checker.options.delete('must_match')
  59. expect(@checker).to be_valid
  60. @checker.options['must_match'] = '2'
  61. expect(@checker).not_to be_valid
  62. expect(@checker.errors[:base].first).to match(/equal to or less than the number of rules/)
  63. end
  64. it "should validate the three fields in each rule" do
  65. @checker.options['rules'] << { 'path' => "foo", 'type' => "fake", 'value' => "6" }
  66. expect(@checker).not_to be_valid
  67. @checker.options['rules'].last['type'] = "field!=value"
  68. expect(@checker).to be_valid
  69. @checker.options['rules'].last.delete('value')
  70. expect(@checker).not_to be_valid
  71. @checker.options['rules'].last['value'] = ''
  72. expect(@checker).to be_valid
  73. @checker.options['rules'].last['value'] = nil
  74. expect(@checker).to be_valid
  75. @checker.options['rules'].last.delete('value')
  76. expect(@checker).not_to be_valid
  77. @checker.options['rules'].last['value'] = ['a']
  78. expect(@checker).to be_valid
  79. @checker.options['rules'].last['path'] = ''
  80. expect(@checker).not_to be_valid
  81. end
  82. it "should validate non-hash rules" do
  83. @checker.options['rules'] << "{% if status == 'ok' %}true{% endif %}"
  84. expect(@checker).to be_valid
  85. @checker.options['rules'] << []
  86. expect(@checker).not_to be_valid
  87. end
  88. end
  89. describe "#working?" do
  90. it "checks to see if the Agent has received any events in the last 'expected_receive_period_in_days' days" do
  91. @event.save!
  92. expect(@checker).not_to be_working # no events have ever been received
  93. Agents::TriggerAgent.async_receive(@checker.id, [@event.id])
  94. expect(@checker.reload).to be_working # Events received
  95. three_days_from_now = 3.days.from_now
  96. allow(Time).to receive(:now) { three_days_from_now }
  97. expect(@checker.reload).not_to be_working # too much time has passed
  98. end
  99. end
  100. describe "#receive" do
  101. it "handles regex" do
  102. @event.payload['foo']['bar']['baz'] = "a222b"
  103. expect {
  104. @checker.receive([@event])
  105. }.not_to(change { Event.count })
  106. @event.payload['foo']['bar']['baz'] = "a2b"
  107. expect {
  108. @checker.receive([@event])
  109. }.to change { Event.count }.by(1)
  110. end
  111. it "handles array of regex" do
  112. @event.payload['foo']['bar']['baz'] = "a222b"
  113. @checker.options['rules'][0] = {
  114. 'type' => "regex",
  115. 'value' => ["a\\db", "a\\Wb"],
  116. 'path' => "foo.bar.baz",
  117. }
  118. expect {
  119. @checker.receive([@event])
  120. }.not_to(change { Event.count })
  121. @event.payload['foo']['bar']['baz'] = "a2b"
  122. expect {
  123. @checker.receive([@event])
  124. }.to change { Event.count }.by(1)
  125. @event.payload['foo']['bar']['baz'] = "a b"
  126. expect {
  127. @checker.receive([@event])
  128. }.to change { Event.count }.by(1)
  129. end
  130. it "handles negated regex" do
  131. @event.payload['foo']['bar']['baz'] = "a2b"
  132. @checker.options['rules'][0] = {
  133. 'type' => "!regex",
  134. 'value' => "a\\db",
  135. 'path' => "foo.bar.baz",
  136. }
  137. expect {
  138. @checker.receive([@event])
  139. }.not_to(change { Event.count })
  140. @event.payload['foo']['bar']['baz'] = "a22b"
  141. expect {
  142. @checker.receive([@event])
  143. }.to change { Event.count }.by(1)
  144. end
  145. it "handles array of negated regex" do
  146. @event.payload['foo']['bar']['baz'] = "a2b"
  147. @checker.options['rules'][0] = {
  148. 'type' => "!regex",
  149. 'value' => ["a\\db", "a2b"],
  150. 'path' => "foo.bar.baz",
  151. }
  152. expect {
  153. @checker.receive([@event])
  154. }.not_to(change { Event.count })
  155. @event.payload['foo']['bar']['baz'] = "a3b"
  156. expect {
  157. @checker.receive([@event])
  158. }.to change { Event.count }.by(1)
  159. end
  160. it "puts can extract values into the message based on paths" do
  161. @checker.receive([@event])
  162. expect(Event.last.payload['message']).to eq("I saw 'a2b' from Joe")
  163. end
  164. it "handles numerical comparisons" do
  165. @event.payload['foo']['bar']['baz'] = "5"
  166. @checker.options['rules'].first['value'] = 6
  167. @checker.options['rules'].first['type'] = "field<value"
  168. expect {
  169. @checker.receive([@event])
  170. }.to change { Event.count }.by(1)
  171. @checker.options['rules'].first['value'] = 3
  172. expect {
  173. @checker.receive([@event])
  174. }.not_to(change { Event.count })
  175. end
  176. it "handles array of numerical comparisons" do
  177. @event.payload['foo']['bar']['baz'] = "5"
  178. @checker.options['rules'].first['value'] = [6, 3]
  179. @checker.options['rules'].first['type'] = "field<value"
  180. expect {
  181. @checker.receive([@event])
  182. }.to change { Event.count }.by(1)
  183. @checker.options['rules'].first['value'] = [4, 3]
  184. expect {
  185. @checker.receive([@event])
  186. }.not_to(change { Event.count })
  187. end
  188. it "handles exact comparisons" do
  189. @event.payload['foo']['bar']['baz'] = "hello world"
  190. @checker.options['rules'].first['type'] = "field==value"
  191. @checker.options['rules'].first['value'] = "hello there"
  192. expect {
  193. @checker.receive([@event])
  194. }.not_to(change { Event.count })
  195. @checker.options['rules'].first['value'] = "hello world"
  196. expect {
  197. @checker.receive([@event])
  198. }.to change { Event.count }.by(1)
  199. end
  200. it "handles array of exact comparisons" do
  201. @event.payload['foo']['bar']['baz'] = "hello world"
  202. @checker.options['rules'].first['type'] = "field==value"
  203. @checker.options['rules'].first['value'] = ["hello there", "hello universe"]
  204. expect {
  205. @checker.receive([@event])
  206. }.not_to(change { Event.count })
  207. @checker.options['rules'].first['value'] = ["hello world", "hello universe"]
  208. expect {
  209. @checker.receive([@event])
  210. }.to change { Event.count }.by(1)
  211. end
  212. it "handles negated comparisons" do
  213. @event.payload['foo']['bar']['baz'] = "hello world"
  214. @checker.options['rules'].first['type'] = "field!=value"
  215. @checker.options['rules'].first['value'] = "hello world"
  216. expect {
  217. @checker.receive([@event])
  218. }.not_to(change { Event.count })
  219. @checker.options['rules'].first['value'] = "hello there"
  220. expect {
  221. @checker.receive([@event])
  222. }.to change { Event.count }.by(1)
  223. end
  224. it "handles array of negated comparisons" do
  225. @event.payload['foo']['bar']['baz'] = "hello world"
  226. @checker.options['rules'].first['type'] = "field!=value"
  227. @checker.options['rules'].first['value'] = ["hello world", "hello world"]
  228. expect {
  229. @checker.receive([@event])
  230. }.not_to(change { Event.count })
  231. @checker.options['rules'].first['value'] = ["hello there", "hello world"]
  232. expect {
  233. @checker.receive([@event])
  234. }.to change { Event.count }.by(1)
  235. end
  236. it "handles array of `not in` comparisons" do
  237. @event.payload['foo']['bar']['baz'] = "hello world"
  238. @checker.options['rules'].first['type'] = "not in"
  239. @checker.options['rules'].first['value'] = ["hello world", "hello world"]
  240. expect {
  241. @checker.receive([@event])
  242. }.not_to(change { Event.count })
  243. @checker.options['rules'].first['value'] = ["hello there", "hello world"]
  244. expect {
  245. @checker.receive([@event])
  246. }.not_to(change { Event.count })
  247. @checker.options['rules'].first['value'] = ["hello there", "hello here"]
  248. expect {
  249. @checker.receive([@event])
  250. }.to change { Event.count }.by(1)
  251. end
  252. it "does fine without dots in the path" do
  253. @event.payload = { 'hello' => "world" }
  254. @checker.options['rules'].first['type'] = "field==value"
  255. @checker.options['rules'].first['path'] = "hello"
  256. @checker.options['rules'].first['value'] = "world"
  257. expect {
  258. @checker.receive([@event])
  259. }.to change { Event.count }.by(1)
  260. @checker.options['rules'].first['path'] = "foo"
  261. expect {
  262. @checker.receive([@event])
  263. }.not_to(change { Event.count })
  264. @checker.options['rules'].first['value'] = "hi"
  265. expect {
  266. @checker.receive([@event])
  267. }.not_to(change { Event.count })
  268. end
  269. it "handles multiple events" do
  270. event2 = Event.new
  271. event2.agent = agents(:bob_weather_agent)
  272. event2.payload = { 'foo' => { 'bar' => { 'baz' => "a2b" } } }
  273. event3 = Event.new
  274. event3.agent = agents(:bob_weather_agent)
  275. event3.payload = { 'foo' => { 'bar' => { 'baz' => "a222b" } } }
  276. expect {
  277. @checker.receive([@event, event2, event3])
  278. }.to change { Event.count }.by(2)
  279. end
  280. it "handles Liquid rules" do
  281. event1 = Event.create!(
  282. agent: agents(:bob_rain_notifier_agent),
  283. payload: { 'hello' => 'world1', 'created_at' => '2019-03-27T08:54:12+09:00' }
  284. )
  285. event2 = Event.create!(
  286. agent: agents(:bob_rain_notifier_agent),
  287. payload: { 'hello' => 'world2', 'created_at' => '2019-03-27T09:17:01+09:00' }
  288. )
  289. @checker.options['message'] = '{{hello}}'
  290. @checker.options['rules'] = [
  291. "{% assign value = created_at | date: '%s' | plus: 0 %}{% assign threshold = 'now' | date: '%s' | minus: 86400 %}{% if value > threshold %}true{% endif %}"
  292. ]
  293. expect {
  294. travel_to(Time.parse('2019-03-28T00:00:00+00:00')) {
  295. @checker.receive([event1, event2])
  296. }
  297. }.to change { Event.count }.by(1)
  298. expect(Event.last.payload['message']).to eq("world2")
  299. end
  300. describe "with multiple rules" do
  301. before do
  302. @checker.options['rules'] << {
  303. 'type' => "field>=value",
  304. 'value' => "4",
  305. 'path' => "foo.bing"
  306. }
  307. end
  308. it "handles ANDing rules together" do
  309. @event.payload['foo']["bing"] = "5"
  310. expect {
  311. @checker.receive([@event])
  312. }.to change { Event.count }.by(1)
  313. @event.payload['foo']["bing"] = "2"
  314. expect {
  315. @checker.receive([@event])
  316. }.not_to(change { Event.count })
  317. end
  318. it "can accept a partial rule set match when 'must_match' is present and less than the total number of rules" do
  319. @checker.options['must_match'] = "1"
  320. @event.payload['foo']["bing"] = "5" # 5 > 4
  321. expect {
  322. @checker.receive([@event])
  323. }.to change { Event.count }.by(1)
  324. @event.payload['foo']["bing"] = "2" # 2 !> 4
  325. expect {
  326. @checker.receive([@event])
  327. }.to(change { Event.count }) # but the first one matches
  328. @checker.options['must_match'] = "2"
  329. @event.payload['foo']["bing"] = "5" # 5 > 4
  330. expect {
  331. @checker.receive([@event])
  332. }.to change { Event.count }.by(1)
  333. @event.payload['foo']["bing"] = "2" # 2 !> 4
  334. expect {
  335. @checker.receive([@event])
  336. }.not_to(change { Event.count }) # only 1 matches, we needed 2
  337. end
  338. end
  339. describe "when 'keep_event' is true" do
  340. before do
  341. @checker.options['keep_event'] = 'true'
  342. @event.payload['foo']['bar']['baz'] = "5"
  343. @checker.options['rules'].first['type'] = "field<value"
  344. end
  345. it "can re-emit the origin event" do
  346. @checker.options['rules'].first['value'] = 3
  347. @checker.options['message'] = ''
  348. @event.payload['message'] = 'hi there'
  349. expect {
  350. @checker.receive([@event])
  351. }.not_to(change { Event.count })
  352. @checker.options['rules'].first['value'] = 6
  353. expect {
  354. @checker.receive([@event])
  355. }.to change { Event.count }.by(1)
  356. expect(@checker.most_recent_event.payload).to eq(@event.payload)
  357. end
  358. it "merges 'message' into the original event when present" do
  359. @checker.options['rules'].first['value'] = 6
  360. @checker.receive([@event])
  361. expect(@checker.most_recent_event.payload).to eq(@event.payload.merge(message: "I saw '5' from Joe"))
  362. end
  363. end
  364. end
  365. end