post_agent_spec.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499
  1. require 'rails_helper'
  2. require 'ostruct'
  3. describe Agents::PostAgent do
  4. before do
  5. @valid_options = {
  6. 'post_url' => "http://www.example.com",
  7. 'expected_receive_period_in_days' => 1,
  8. 'payload' => {
  9. 'default' => 'value'
  10. }
  11. }
  12. @valid_params = {
  13. name: "somename",
  14. options: @valid_options
  15. }
  16. @checker = Agents::PostAgent.new(@valid_params)
  17. @checker.user = users(:jane)
  18. @checker.save!
  19. @event = Event.new
  20. @event.agent = agents(:jane_weather_agent)
  21. @event.payload = {
  22. 'somekey' => 'somevalue',
  23. 'someotherkey' => {
  24. 'somekey' => 'value'
  25. }
  26. }
  27. @requests = 0
  28. @sent_requests = Hash.new { |hash, method| hash[method] = [] }
  29. stub_request(:any, /:/).to_return { |request|
  30. method = request.method
  31. @requests += 1
  32. @sent_requests[method] << req = OpenStruct.new(uri: request.uri, headers: request.headers)
  33. case method
  34. when :get, :delete
  35. req.data = request.uri.query
  36. else
  37. content_type = request.headers['Content-Type'][/\A[^;\s]+/]
  38. case content_type
  39. when 'application/x-www-form-urlencoded'
  40. req.data = request.body
  41. when 'application/json'
  42. req.data = ActiveSupport::JSON.decode(request.body)
  43. when 'text/xml'
  44. req.data = Hash.from_xml(request.body)
  45. when Agents::PostAgent::MIME_RE
  46. req.data = request.body
  47. else
  48. raise "unexpected Content-Type: #{content_type}"
  49. end
  50. end
  51. { status: 200, body: "<html>a webpage!</html>", headers: { 'Content-type' => 'text/html', 'X-Foo-Bar' => 'baz' } }
  52. }
  53. end
  54. it_behaves_like WebRequestConcern
  55. it_behaves_like 'FileHandlingConsumer'
  56. it 'renders the description markdown without errors' do
  57. expect { @checker.description }.not_to raise_error
  58. end
  59. describe "making requests" do
  60. it "can make requests of each type" do
  61. %w[get put post patch delete].each.with_index(1) do |verb, index|
  62. @checker.options['method'] = verb
  63. expect(@checker).to be_valid
  64. @checker.check
  65. expect(@requests).to eq(index)
  66. expect(@sent_requests[verb.to_sym].length).to eq(1)
  67. end
  68. end
  69. end
  70. describe "#receive" do
  71. it "can handle multiple events and merge the payloads with options['payload']" do
  72. event1 = Event.new
  73. event1.agent = agents(:bob_weather_agent)
  74. event1.payload = {
  75. 'xyz' => 'value1',
  76. 'message' => 'value2',
  77. 'default' => 'value2'
  78. }
  79. expect {
  80. expect {
  81. @checker.receive([@event, event1])
  82. }.to change { @sent_requests[:post].length }.by(2)
  83. }.not_to change { @sent_requests[:get].length }
  84. expect(@sent_requests[:post][0].data).to eq(@event.payload.merge('default' => 'value').to_query)
  85. expect(@sent_requests[:post][1].data).to eq(event1.payload.to_query)
  86. end
  87. it "can make GET requests" do
  88. @checker.options['method'] = 'get'
  89. expect {
  90. expect {
  91. @checker.receive([@event])
  92. }.to change { @sent_requests[:get].length }.by(1)
  93. }.not_to change { @sent_requests[:post].length }
  94. expect(@sent_requests[:get][0].data).to eq(@event.payload.merge('default' => 'value').to_query)
  95. end
  96. it "can make a GET request merging params in post_url, payload and event" do
  97. @checker.options['method'] = 'get'
  98. @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value"
  99. @event.payload = {
  100. "some_param" => "some_value",
  101. "another_param" => "another_value"
  102. }
  103. @checker.receive([@event])
  104. uri = @sent_requests[:get].first.uri
  105. # parameters are alphabetically sorted by Faraday
  106. expect(uri.request_uri).to eq("/a/path?another_param=another_value&default=value&existing_param=existing_value&some_param=some_value")
  107. end
  108. it "can skip merging the incoming event when no_merge is set, but it still interpolates" do
  109. @checker.options['no_merge'] = 'true'
  110. @checker.options['payload'] = {
  111. 'key' => 'it said: {{ someotherkey.somekey }}'
  112. }
  113. @checker.receive([@event])
  114. expect(@sent_requests[:post].first.data).to eq({ 'key' => 'it said: value' }.to_query)
  115. end
  116. it "interpolates when receiving a payload" do
  117. @checker.options['post_url'] = "https://{{ domain }}/{{ variable }}?existing_param=existing_value"
  118. @event.payload = {
  119. 'domain' => 'google.com',
  120. 'variable' => 'a_variable'
  121. }
  122. @checker.receive([@event])
  123. uri = @sent_requests[:post].first.uri
  124. expect(uri.scheme).to eq('https')
  125. expect(uri.host).to eq('google.com')
  126. expect(uri.path).to eq('/a_variable')
  127. expect(uri.query).to eq("existing_param=existing_value")
  128. end
  129. it "interpolates outgoing headers with the event payload" do
  130. @checker.options['headers'] = {
  131. "Foo" => "{{ variable }}"
  132. }
  133. @event.payload = {
  134. 'variable' => 'a_variable'
  135. }
  136. @checker.receive([@event])
  137. headers = @sent_requests[:post].first.headers
  138. expect(headers["Foo"]).to eq("a_variable")
  139. end
  140. it 'makes a multipart request when receiving a file_pointer' do
  141. WebMock.reset!
  142. stub_request(:post, "http://www.example.com/").
  143. with(headers: {
  144. 'Accept-Encoding' => 'gzip,deflate',
  145. 'Content-Type' => /\Amultipart\/form-data; boundary=/,
  146. 'User-Agent' => 'Huginn - https://github.com/huginn/huginn'
  147. }) { |request|
  148. qboundary = Regexp.quote(request.headers['Content-Type'][/ boundary=(.+)/, 1])
  149. /\A--#{qboundary}\r\nContent-Disposition: form-data; name="default"\r\n\r\nvalue\r\n--#{qboundary}\r\nContent-Disposition: form-data; name="file"; filename="local.path"\r\nContent-Length: 8\r\nContent-Type: \r\nContent-Transfer-Encoding: binary\r\n\r\ntestdata\r\n--#{qboundary}--\r\n\r\n\z/ === request.body
  150. }.to_return(status: 200, body: "", headers: {})
  151. event = Event.new(payload: {file_pointer: {agent_id: 111, file: 'test'}})
  152. io_mock = mock()
  153. mock(@checker).get_io(event) { StringIO.new("testdata") }
  154. @checker.options['no_merge'] = true
  155. @checker.receive([event])
  156. end
  157. end
  158. describe "#check" do
  159. it "sends options['payload'] as a POST request" do
  160. expect {
  161. @checker.check
  162. }.to change { @sent_requests[:post].length }.by(1)
  163. expect(@sent_requests[:post][0].data).to eq(@checker.options['payload'].to_query)
  164. end
  165. it "sends options['payload'] as JSON as a POST request" do
  166. @checker.options['content_type'] = 'json'
  167. expect {
  168. @checker.check
  169. }.to change { @sent_requests[:post].length }.by(1)
  170. expect(@sent_requests[:post][0].data).to eq(@checker.options['payload'])
  171. end
  172. it "sends options['payload'] as XML as a POST request" do
  173. @checker.options['content_type'] = 'xml'
  174. expect {
  175. @checker.check
  176. }.to change { @sent_requests[:post].length }.by(1)
  177. expect(@sent_requests[:post][0].data.keys).to eq([ 'post' ])
  178. expect(@sent_requests[:post][0].data['post']).to eq(@checker.options['payload'])
  179. end
  180. it "sends options['payload'] as XML with custom root element name, as a POST request" do
  181. @checker.options['content_type'] = 'xml'
  182. @checker.options['xml_root'] = 'foobar'
  183. expect {
  184. @checker.check
  185. }.to change { @sent_requests[:post].length }.by(1)
  186. expect(@sent_requests[:post][0].data.keys).to eq([ 'foobar' ])
  187. expect(@sent_requests[:post][0].data['foobar']).to eq(@checker.options['payload'])
  188. end
  189. it "sends options['payload'] as a GET request" do
  190. @checker.options['method'] = 'get'
  191. expect {
  192. expect {
  193. @checker.check
  194. }.to change { @sent_requests[:get].length }.by(1)
  195. }.not_to change { @sent_requests[:post].length }
  196. expect(@sent_requests[:get][0].data).to eq(@checker.options['payload'].to_query)
  197. end
  198. it "sends options['payload'] as a string POST request when content-type continas a MIME type" do
  199. @checker.options['payload'] = '<test>hello</test>'
  200. @checker.options['content_type'] = 'application/xml'
  201. expect {
  202. @checker.check
  203. }.to change { @sent_requests[:post].length }.by(1)
  204. expect(@sent_requests[:post][0].data).to eq('<test>hello</test>')
  205. end
  206. it "interpolates outgoing headers" do
  207. @checker.options['headers'] = {
  208. "Foo" => "{% credential aws_key %}"
  209. }
  210. @checker.check
  211. headers = @sent_requests[:post].first.headers
  212. expect(headers["Foo"]).to eq("2222222222-jane")
  213. end
  214. describe "emitting events" do
  215. context "when emit_events is not set to true" do
  216. it "does not emit events" do
  217. expect {
  218. @checker.check
  219. }.not_to change { @checker.events.count }
  220. end
  221. end
  222. context "when emit_events is set to true" do
  223. before do
  224. @checker.options['emit_events'] = 'true'
  225. @checker.save!
  226. end
  227. it "emits the response status" do
  228. expect {
  229. @checker.check
  230. }.to change { @checker.events.count }.by(1)
  231. expect(@checker.events.last.payload['status']).to eq 200
  232. end
  233. it "emits the body" do
  234. @checker.check
  235. expect(@checker.events.last.payload['body']).to eq '<html>a webpage!</html>'
  236. end
  237. it "emits the response headers capitalized by default" do
  238. @checker.check
  239. expect(@checker.events.last.payload['headers']).to eq({ 'Content-Type' => 'text/html', 'X-Foo-Bar' => 'baz' })
  240. end
  241. it "emits the response headers capitalized" do
  242. @checker.options['event_headers_style'] = 'capitalized'
  243. @checker.check
  244. expect(@checker.events.last.payload['headers']).to eq({ 'Content-Type' => 'text/html', 'X-Foo-Bar' => 'baz' })
  245. end
  246. it "emits the response headers downcased" do
  247. @checker.options['event_headers_style'] = 'downcased'
  248. @checker.check
  249. expect(@checker.events.last.payload['headers']).to eq({ 'content-type' => 'text/html', 'x-foo-bar' => 'baz' })
  250. end
  251. it "emits the response headers snakecased" do
  252. @checker.options['event_headers_style'] = 'snakecased'
  253. @checker.check
  254. expect(@checker.events.last.payload['headers']).to eq({ 'content_type' => 'text/html', 'x_foo_bar' => 'baz' })
  255. end
  256. it "emits the response headers only including those specified by event_headers" do
  257. @checker.options['event_headers_style'] = 'snakecased'
  258. @checker.options['event_headers'] = 'content-type'
  259. @checker.check
  260. expect(@checker.events.last.payload['headers']).to eq({ 'content_type' => 'text/html' })
  261. end
  262. context "when output_mode is set to 'merge'" do
  263. before do
  264. @checker.options['output_mode'] = 'merge'
  265. @checker.save!
  266. end
  267. it "emits the received event" do
  268. @checker.receive([@event])
  269. @checker.check
  270. expect(@checker.events.last.payload['somekey']).to eq('somevalue')
  271. expect(@checker.events.last.payload['someotherkey']).to eq({ 'somekey' => 'value' })
  272. end
  273. end
  274. end
  275. end
  276. end
  277. describe "#working?" do
  278. it "checks if there was an error" do
  279. @checker.error("error")
  280. expect(@checker.logs.count).to eq(1)
  281. expect(@checker.reload).not_to be_working
  282. end
  283. it "checks if 'expected_receive_period_in_days' was not set" do
  284. expect(@checker.logs.count).to eq(0)
  285. @checker.options.delete('expected_receive_period_in_days')
  286. expect(@checker).to be_working
  287. end
  288. it "checks if no event has been received" do
  289. expect(@checker.logs.count).to eq(0)
  290. expect(@checker.last_receive_at).to be_nil
  291. expect(@checker.reload).not_to be_working
  292. end
  293. it "checks if events have been received within expected receive period" do
  294. expect(@checker).not_to be_working
  295. Agents::PostAgent.async_receive @checker.id, [@event.id]
  296. expect(@checker.reload).to be_working
  297. two_days_from_now = 2.days.from_now
  298. stub(Time).now { two_days_from_now }
  299. expect(@checker.reload).not_to be_working
  300. end
  301. end
  302. describe "validation" do
  303. before do
  304. expect(@checker).to be_valid
  305. end
  306. it "should validate presence of post_url" do
  307. @checker.options['post_url'] = ""
  308. expect(@checker).not_to be_valid
  309. end
  310. it "should validate absence of expected_receive_period_in_days is allowed" do
  311. @checker.options['expected_receive_period_in_days'] = ""
  312. expect(@checker).to be_valid
  313. end
  314. it "should validate method as post, get, put, patch, or delete, defaulting to post" do
  315. @checker.options['method'] = ""
  316. expect(@checker.method).to eq("post")
  317. expect(@checker).to be_valid
  318. @checker.options['method'] = "POST"
  319. expect(@checker.method).to eq("post")
  320. expect(@checker).to be_valid
  321. @checker.options['method'] = "get"
  322. expect(@checker.method).to eq("get")
  323. expect(@checker).to be_valid
  324. @checker.options['method'] = "patch"
  325. expect(@checker.method).to eq("patch")
  326. expect(@checker).to be_valid
  327. @checker.options['method'] = "wut"
  328. expect(@checker.method).to eq("wut")
  329. expect(@checker).not_to be_valid
  330. end
  331. it "should validate that no_merge is 'true' or 'false', if present" do
  332. @checker.options['no_merge'] = ""
  333. expect(@checker).to be_valid
  334. @checker.options['no_merge'] = "true"
  335. expect(@checker).to be_valid
  336. @checker.options['no_merge'] = "false"
  337. expect(@checker).to be_valid
  338. @checker.options['no_merge'] = false
  339. expect(@checker).to be_valid
  340. @checker.options['no_merge'] = true
  341. expect(@checker).to be_valid
  342. @checker.options['no_merge'] = 'blarg'
  343. expect(@checker).not_to be_valid
  344. end
  345. it "should validate payload as a hash, if present" do
  346. @checker.options['payload'] = ""
  347. expect(@checker).to be_valid
  348. @checker.options['payload'] = ["foo", "bar"]
  349. expect(@checker).to be_valid
  350. @checker.options['payload'] = "hello"
  351. expect(@checker).not_to be_valid
  352. @checker.options['payload'] = { 'this' => 'that' }
  353. expect(@checker).to be_valid
  354. end
  355. it "should not validate payload as a hash or an array if content_type includes a MIME type and method is not get or delete" do
  356. @checker.options['no_merge'] = 'true'
  357. @checker.options['content_type'] = 'text/xml'
  358. @checker.options['payload'] = "test"
  359. expect(@checker).to be_valid
  360. @checker.options['method'] = 'get'
  361. expect(@checker).not_to be_valid
  362. @checker.options['method'] = 'delete'
  363. expect(@checker).not_to be_valid
  364. end
  365. it "requires `no_merge` to be set to true when content_type contains a MIME type" do
  366. @checker.options['content_type'] = 'text/xml'
  367. @checker.options['payload'] = "test"
  368. expect(@checker).not_to be_valid
  369. end
  370. it "requires headers to be a hash, if present" do
  371. @checker.options['headers'] = [1,2,3]
  372. expect(@checker).not_to be_valid
  373. @checker.options['headers'] = "hello world"
  374. expect(@checker).not_to be_valid
  375. @checker.options['headers'] = ""
  376. expect(@checker).to be_valid
  377. @checker.options['headers'] = {}
  378. expect(@checker).to be_valid
  379. @checker.options['headers'] = { "Authorization" => "foo bar" }
  380. expect(@checker).to be_valid
  381. end
  382. it "requires emit_events to be true or false" do
  383. @checker.options['emit_events'] = 'what?'
  384. expect(@checker).not_to be_valid
  385. @checker.options.delete('emit_events')
  386. expect(@checker).to be_valid
  387. @checker.options['emit_events'] = 'true'
  388. expect(@checker).to be_valid
  389. @checker.options['emit_events'] = 'false'
  390. expect(@checker).to be_valid
  391. @checker.options['emit_events'] = true
  392. expect(@checker).to be_valid
  393. end
  394. it "requires output_mode to be 'clean' or 'merge', if present" do
  395. @checker.options['output_mode'] = 'what?'
  396. expect(@checker).not_to be_valid
  397. @checker.options.delete('output_mode')
  398. expect(@checker).to be_valid
  399. @checker.options['output_mode'] = 'clean'
  400. expect(@checker).to be_valid
  401. @checker.options['output_mode'] = 'merge'
  402. expect(@checker).to be_valid
  403. @checker.options['output_mode'] = :clean
  404. expect(@checker).to be_valid
  405. @checker.options['output_mode'] = :merge
  406. expect(@checker).to be_valid
  407. @checker.options['output_mode'] = '{{somekey}}'
  408. expect(@checker).to be_valid
  409. @checker.options['output_mode'] = "{% if key == 'foo' %}merge{% else %}clean{% endif %}"
  410. expect(@checker).to be_valid
  411. end
  412. end
  413. end