require 'rails_helper' require 'ostruct' describe Agents::PostAgent do let(:mocked_response) do { status: 200, body: "a webpage!", headers: { 'Content-type' => 'text/html', 'X-Foo-Bar' => 'baz', } } end before do @valid_options = { 'post_url' => "http://www.example.com", 'expected_receive_period_in_days' => 1, 'payload' => { 'default' => 'value' } } @valid_params = { name: "somename", options: @valid_options } @checker = Agents::PostAgent.new(@valid_params) @checker.user = users(:jane) @checker.save! @event = Event.new @event.agent = agents(:jane_weather_agent) @event.payload = { 'somekey' => 'somevalue', 'someotherkey' => { 'somekey' => 'value' } } @requests = 0 @sent_requests = Hash.new { |hash, method| hash[method] = [] } stub_request(:any, /:/).to_return { |request| method = request.method @requests += 1 @sent_requests[method] << req = OpenStruct.new(uri: request.uri, headers: request.headers) case method when :get, :delete req.data = request.uri.query else content_type = request.headers['Content-Type'][/\A[^;\s]+/] case content_type when 'application/x-www-form-urlencoded' req.data = request.body when 'application/json' req.data = ActiveSupport::JSON.decode(request.body) when 'text/xml' req.data = Hash.from_xml(request.body) when Agents::PostAgent::MIME_RE req.data = request.body else raise "unexpected Content-Type: #{content_type}" end end mocked_response } end it_behaves_like WebRequestConcern it_behaves_like 'FileHandlingConsumer' it 'renders the description markdown without errors' do expect { @checker.description }.not_to raise_error end describe "making requests" do it "can make requests of each type" do %w[get put post patch delete].each.with_index(1) do |verb, index| @checker.options['method'] = verb expect(@checker).to be_valid @checker.check expect(@requests).to eq(index) expect(@sent_requests[verb.to_sym].length).to eq(1) end end end describe "#receive" do it "can handle multiple events and merge the payloads with options['payload']" do event1 = Event.new event1.agent = agents(:bob_weather_agent) event1.payload = { 'xyz' => 'value1', 'message' => 'value2', 'default' => 'value2' } expect { expect { @checker.receive([@event, event1]) }.to change { @sent_requests[:post].length }.by(2) }.not_to(change { @sent_requests[:get].length }) expect(@sent_requests[:post][0].data).to eq(@event.payload.merge('default' => 'value').to_query) expect(@sent_requests[:post][1].data).to eq(event1.payload.to_query) end it "can make GET requests" do @checker.options['method'] = 'get' expect { expect { @checker.receive([@event]) }.to change { @sent_requests[:get].length }.by(1) }.not_to(change { @sent_requests[:post].length }) expect(@sent_requests[:get][0].data).to eq(@event.payload.merge('default' => 'value').to_query) end it "can make a GET request merging params in post_url, payload and event" do @checker.options['method'] = 'get' @checker.options['post_url'] = "http://example.com/a/path?existing_param=existing_value" @event.payload = { "some_param" => "some_value", "another_param" => "another_value" } @checker.receive([@event]) uri = @sent_requests[:get].first.uri # parameters are alphabetically sorted by Faraday expect(uri.request_uri).to eq("/a/path?another_param=another_value&default=value&existing_param=existing_value&some_param=some_value") end it "can skip merging the incoming event when no_merge is set, but it still interpolates" do @checker.options['no_merge'] = 'true' @checker.options['payload'] = { 'key' => 'it said: {{ someotherkey.somekey }}' } @checker.receive([@event]) expect(@sent_requests[:post].first.data).to eq({ 'key' => 'it said: value' }.to_query) end it "interpolates when receiving a payload" do @checker.options['post_url'] = "https://{{ domain }}/{{ variable }}?existing_param=existing_value" @event.payload = { 'domain' => 'google.com', 'variable' => 'a_variable' } @checker.receive([@event]) uri = @sent_requests[:post].first.uri expect(uri.scheme).to eq('https') expect(uri.host).to eq('google.com') expect(uri.path).to eq('/a_variable') expect(uri.query).to eq("existing_param=existing_value") end it "interpolates outgoing headers with the event payload" do @checker.options['headers'] = { "Foo" => "{{ variable }}" } @event.payload = { 'variable' => 'a_variable' } @checker.receive([@event]) headers = @sent_requests[:post].first.headers expect(headers["Foo"]).to eq("a_variable") end it 'makes a multipart request when receiving a file_pointer' do WebMock.reset! stub_request(:post, "http://www.example.com/") .with(headers: { 'Accept-Encoding' => 'gzip,deflate', 'Content-Type' => /\Amultipart\/form-data; boundary=/, 'User-Agent' => 'Huginn - https://github.com/huginn/huginn' }) { |request| qboundary = Regexp.quote(request.headers['Content-Type'][/ boundary=(.+)/, 1]) /\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\z/ === request.body }.to_return(status: 200, body: "", headers: {}) event = Event.new(payload: { file_pointer: { agent_id: 111, file: 'test' } }) io_mock = double expect(@checker).to receive(:get_io).with(event) { StringIO.new("testdata") } @checker.options['no_merge'] = true @checker.receive([event]) end end describe "#check" do it "sends options['payload'] as a POST request" do expect { @checker.check }.to change { @sent_requests[:post].length }.by(1) expect(@sent_requests[:post][0].data).to eq(@checker.options['payload'].to_query) end it "sends options['payload'] as JSON as a POST request" do @checker.options['content_type'] = 'json' expect { @checker.check }.to change { @sent_requests[:post].length }.by(1) expect(@sent_requests[:post][0].data).to eq(@checker.options['payload']) end it "sends options['payload'] as XML as a POST request" do @checker.options['content_type'] = 'xml' expect { @checker.check }.to change { @sent_requests[:post].length }.by(1) expect(@sent_requests[:post][0].data.keys).to eq(['post']) expect(@sent_requests[:post][0].data['post']).to eq(@checker.options['payload']) end it "sends options['payload'] as XML with custom root element name, as a POST request" do @checker.options['content_type'] = 'xml' @checker.options['xml_root'] = 'foobar' expect { @checker.check }.to change { @sent_requests[:post].length }.by(1) expect(@sent_requests[:post][0].data.keys).to eq(['foobar']) expect(@sent_requests[:post][0].data['foobar']).to eq(@checker.options['payload']) end it "sends options['payload'] as a GET request" do @checker.options['method'] = 'get' expect { expect { @checker.check }.to change { @sent_requests[:get].length }.by(1) }.not_to(change { @sent_requests[:post].length }) expect(@sent_requests[:get][0].data).to eq(@checker.options['payload'].to_query) end it "sends options['payload'] as a string POST request when content-type continas a MIME type" do @checker.options['payload'] = 'hello' @checker.options['content_type'] = 'application/xml' expect { @checker.check }.to change { @sent_requests[:post].length }.by(1) expect(@sent_requests[:post][0].data).to eq('hello') end it "interpolates outgoing headers" do @checker.options['headers'] = { "Foo" => "{% credential aws_key %}" } @checker.check headers = @sent_requests[:post].first.headers expect(headers["Foo"]).to eq("2222222222-jane") end describe "emitting events" do context "when emit_events is not set to true" do it "does not emit events" do expect { @checker.check }.not_to(change { @checker.events.count }) end end context "when emit_events is set to true" do before do @checker.options['emit_events'] = 'true' @checker.save! end it "emits the response status" do expect { @checker.check }.to change { @checker.events.count }.by(1) expect(@checker.events.last.payload['status']).to eq 200 end it "emits the body" do @checker.check expect(@checker.events.last.payload['body']).to eq 'a webpage!' end context "and the response is in JSON" do let(:json_data) { { "foo" => 123, "bar" => 456 } } let(:mocked_response) do { status: 200, body: json_data.to_json, headers: { 'Content-type' => 'application/json', 'X-Foo-Bar' => 'baz', } } end it "emits the unparsed JSON body" do @checker.check expect(@checker.events.last.payload['body']).to eq json_data.to_json end it "emits the parsed JSON body when parse_body is true" do @checker.options['parse_body'] = 'true' @checker.save! @checker.check expect(@checker.events.last.payload['body']).to eq json_data end end it "emits the response headers capitalized by default" do @checker.check expect(@checker.events.last.payload['headers']).to eq({ 'Content-Type' => 'text/html', 'X-Foo-Bar' => 'baz' }) end it "emits the response headers capitalized" do @checker.options['event_headers_style'] = 'capitalized' @checker.check expect(@checker.events.last.payload['headers']).to eq({ 'Content-Type' => 'text/html', 'X-Foo-Bar' => 'baz' }) end it "emits the response headers downcased" do @checker.options['event_headers_style'] = 'downcased' @checker.check expect(@checker.events.last.payload['headers']).to eq({ 'content-type' => 'text/html', 'x-foo-bar' => 'baz' }) end it "emits the response headers snakecased" do @checker.options['event_headers_style'] = 'snakecased' @checker.check expect(@checker.events.last.payload['headers']).to eq({ 'content_type' => 'text/html', 'x_foo_bar' => 'baz' }) end it "emits the response headers only including those specified by event_headers" do @checker.options['event_headers_style'] = 'snakecased' @checker.options['event_headers'] = 'content-type' @checker.check expect(@checker.events.last.payload['headers']).to eq({ 'content_type' => 'text/html' }) end context "when output_mode is set to 'merge'" do before do @checker.options['output_mode'] = 'merge' @checker.save! end it "emits the received event" do @checker.receive([@event]) @checker.check expect(@checker.events.last.payload['somekey']).to eq('somevalue') expect(@checker.events.last.payload['someotherkey']).to eq({ 'somekey' => 'value' }) end end end end end describe "#working?" do it "checks if there was an error" do @checker.error("error") expect(@checker.logs.count).to eq(1) expect(@checker.reload).not_to be_working end it "checks if 'expected_receive_period_in_days' was not set" do expect(@checker.logs.count).to eq(0) @checker.options.delete('expected_receive_period_in_days') expect(@checker).to be_working end it "checks if no event has been received" do expect(@checker.logs.count).to eq(0) expect(@checker.last_receive_at).to be_nil expect(@checker.reload).not_to be_working end it "checks if events have been received within expected receive period" do expect(@checker).not_to be_working Agents::PostAgent.async_receive @checker.id, [@event.id] expect(@checker.reload).to be_working two_days_from_now = 2.days.from_now allow(Time).to receive(:now) { two_days_from_now } expect(@checker.reload).not_to be_working end end describe "validation" do before do expect(@checker).to be_valid end it "should validate presence of post_url" do @checker.options['post_url'] = "" expect(@checker).not_to be_valid end it "should validate absence of expected_receive_period_in_days is allowed" do @checker.options['expected_receive_period_in_days'] = "" expect(@checker).to be_valid end it "should validate method as post, get, put, patch, or delete, defaulting to post" do @checker.options['method'] = "" expect(@checker.method).to eq("post") expect(@checker).to be_valid @checker.options['method'] = "POST" expect(@checker.method).to eq("post") expect(@checker).to be_valid @checker.options['method'] = "get" expect(@checker.method).to eq("get") expect(@checker).to be_valid @checker.options['method'] = "patch" expect(@checker.method).to eq("patch") expect(@checker).to be_valid @checker.options['method'] = "wut" expect(@checker.method).to eq("wut") expect(@checker).not_to be_valid end it "should validate that no_merge is 'true' or 'false', if present" do @checker.options['no_merge'] = "" expect(@checker).to be_valid @checker.options['no_merge'] = "true" expect(@checker).to be_valid @checker.options['no_merge'] = "false" expect(@checker).to be_valid @checker.options['no_merge'] = false expect(@checker).to be_valid @checker.options['no_merge'] = true expect(@checker).to be_valid @checker.options['no_merge'] = 'blarg' expect(@checker).not_to be_valid end it "should validate payload as a hash, if present" do @checker.options['payload'] = "" expect(@checker).to be_valid @checker.options['payload'] = ["foo", "bar"] expect(@checker).to be_valid @checker.options['payload'] = "hello" expect(@checker).not_to be_valid @checker.options['payload'] = { 'this' => 'that' } expect(@checker).to be_valid end 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 @checker.options['no_merge'] = 'true' @checker.options['content_type'] = 'text/xml' @checker.options['payload'] = "test" expect(@checker).to be_valid @checker.options['method'] = 'get' expect(@checker).not_to be_valid @checker.options['method'] = 'delete' expect(@checker).not_to be_valid end it "requires `no_merge` to be set to true when content_type contains a MIME type" do @checker.options['content_type'] = 'text/xml' @checker.options['payload'] = "test" expect(@checker).not_to be_valid end it "requires headers to be a hash, if present" do @checker.options['headers'] = [1, 2, 3] expect(@checker).not_to be_valid @checker.options['headers'] = "hello world" expect(@checker).not_to be_valid @checker.options['headers'] = "" expect(@checker).to be_valid @checker.options['headers'] = {} expect(@checker).to be_valid @checker.options['headers'] = { "Authorization" => "foo bar" } expect(@checker).to be_valid end it "requires emit_events to be true or false" do @checker.options['emit_events'] = 'what?' expect(@checker).not_to be_valid @checker.options.delete('emit_events') expect(@checker).to be_valid @checker.options['emit_events'] = 'true' expect(@checker).to be_valid @checker.options['emit_events'] = 'false' expect(@checker).to be_valid @checker.options['emit_events'] = true expect(@checker).to be_valid end it "requires output_mode to be 'clean' or 'merge', if present" do @checker.options['output_mode'] = 'what?' expect(@checker).not_to be_valid @checker.options.delete('output_mode') expect(@checker).to be_valid @checker.options['output_mode'] = 'clean' expect(@checker).to be_valid @checker.options['output_mode'] = 'merge' expect(@checker).to be_valid @checker.options['output_mode'] = :clean expect(@checker).to be_valid @checker.options['output_mode'] = :merge expect(@checker).to be_valid @checker.options['output_mode'] = '{{somekey}}' expect(@checker).to be_valid @checker.options['output_mode'] = "{% if key == 'foo' %}merge{% else %}clean{% endif %}" expect(@checker).to be_valid end end end