require 'spec_helper' describe Honeybadger::Sender do before { reset_config } let(:http) { stub_http } it "makes a single request when sending notices" do http.should_receive(:post).with(Honeybadger::Sender::NOTICES_URI, kind_of(String), kind_of(Hash)) Honeybadger.notify(RuntimeError.new('oops!')) end it "sends a user agent with version number" do http = stub_http http.should_receive(:post).with(kind_of(String), kind_of(String), hash_including({'User-Agent' => "HB-Ruby #{Honeybadger::VERSION}; #{RUBY_VERSION}; #{RUBY_PLATFORM}"})) send_exception end it "posts to Honeybadger when using an HTTP proxy" do http = stub_http proxy = double(:new => http) Net::HTTP.stub(:Proxy).and_return(proxy) http.should_receive(:post).with(Honeybadger::Sender::NOTICES_URI, kind_of(String), Honeybadger::HEADERS.merge({ 'X-API-Key' => 'abc123'})) Net::HTTP.should_receive(:Proxy).with('some.host', 88, 'login', 'passwd') send_exception(:proxy_host => 'some.host', :proxy_port => 88, :proxy_user => 'login', :proxy_pass => 'passwd', :notice => 'asdf') end it "returns the created group's id on successful posting" do stub_http(:body => '{"id":"3799307"}') expect(send_exception(:secure => false)).to eq '3799307' end it "returns nil on failed posting" do stub_http(:response => Net::HTTPError) expect(send_exception(:secure => false)).to be_nil end describe '#api_key' do context 'api_key is missing' do it "logs missing API key and return nil" do sender = build_sender(:api_key => nil) sender.should_receive(:log).with(:error, /API key/) expect(send_exception(:sender => sender, :secure => false)).to be_nil end end context 'notice is a hash' do it 'uses api_key from hash when present' do sender = build_sender(:api_key => 'asdf') http.should_receive(:post).with(Honeybadger::Sender::NOTICES_URI, kind_of(String), hash_including('X-API-Key' => 'zxcv')) send_exception(:sender => sender, :notice => { 'api_key' => 'zxcv' }) end end context 'notice is a Honeybadger::Notice' do it 'uses api_key from notice when present' do sender = build_sender(:api_key => 'asdf') http.should_receive(:post).with(Honeybadger::Sender::NOTICES_URI, kind_of(String), hash_including('X-API-Key' => 'zxcv')) send_exception(:sender => sender, :notice => Honeybadger::Notice.new(:api_key => 'zxcv')) end end end context "success response from server" do let(:sender) { build_sender } before { stub_http } it "logs success" do sender.should_receive(:log).with(:debug, /Success/, kind_of(Net::HTTPSuccess), kind_of(String)) send_exception(:sender => sender, :secure => false) end it "doesn't change features" do expect { send_exception(:sender => sender, :secure => false) }.not_to change { Honeybadger.configuration.features } end end context "403 response from server" do it "deactivates notices on 403" do stub_http(:response => Net::HTTPForbidden.new('1.2', '403', 'Forbidden')) sender = build_sender expect { send_exception(:sender => sender, :secure => false) }.to change { Honeybadger.configuration.features['notices'] }.to false end end it "logs failure" do stub_http(:response => Net::HTTPServerError.new('1.2', '500', 'Internal Error')) sender = build_sender sender.should_receive(:log).with(:error, /Failure/, kind_of(Net::HTTPServerError), kind_of(String)) send_exception(:sender => sender, :secure => false) end context "when encountering exceptions" do # TODO: Figure out why nested groups aren't running context "HTTP connection setup problems" do it "should not be rescued" do proxy = double() proxy.stub(:new).and_raise(NoMemoryError) Net::HTTP.stub(:Proxy).and_return(proxy) expect { build_sender.send(:setup_http_connection) }.to raise_error(NoMemoryError) end it "should be logged" do proxy = double() proxy.stub(:new).and_raise(RuntimeError) Net::HTTP.stub(:Proxy).and_return(proxy) sender = build_sender sender.should_receive(:log).with(:error, /Failure initializing the HTTP connection/) expect { sender.send(:setup_http_connection) }.to raise_error(RuntimeError) end end context "unexpected exception sending problems" do it "should be logged" do sender = build_sender sender.should_receive(:setup_http_connection).and_raise(RuntimeError) sender.should_receive(:log).with(:error, /Error/) sender.send_to_honeybadger("stuff") end it "should log the exception on any error" do Honeybadger.configuration.log_exception_on_send_failure = true notice = Honeybadger::Notice.new(:exception => Exception.new("bad things")) sender = build_sender sender.should_receive(:setup_http_connection).and_raise(RuntimeError) sender.stub(:log) Honeybadger.should_receive(:write_verbose_log).with(/Original Exception:.*bad things/, :error) sender.send_to_honeybadger(notice) end it "should not log the exception on any error by default" do notice = Honeybadger::Notice.new(:exception => Exception.new("bad things")) sender = build_sender sender.should_receive(:setup_http_connection).and_raise(RuntimeError) sender.stub(:log) Honeybadger.should_not_receive(:write_verbose_log).with(/Original Exception:.*bad things/, :error) sender.send_to_honeybadger(notice) end it "should log the exception on a non-successful HTTP response" do Honeybadger.configuration.log_exception_on_send_failure = true stub_http(:response => Net::HTTPError) notice = Honeybadger::Notice.new(:exception => Exception.new("bad things")) sender = build_sender sender.stub(:log) Honeybadger.should_receive(:write_verbose_log).with(/Original Exception:.*bad things/, :error) sender.send_to_honeybadger(notice) end it "returns nil no matter what" do sender = build_sender sender.should_receive(:setup_http_connection).and_raise(LocalJumpError) expect { sender.send_to_honeybadger("stuff").should be_nil }.not_to raise_error end end it "returns nil on failed posting" do http = stub_http http.should_receive(:post).and_raise(Errno::ECONNREFUSED) expect(send_exception(:secure => false)).to be_nil end it "not fail when posting and a timeout exception occurs" do http = stub_http http.should_receive(:post).and_raise(TimeoutError) expect { send_exception(:secure => false) }.not_to raise_error end it "not fail when posting and a connection refused exception occurs" do http = stub_http http.should_receive(:post).and_raise(Errno::ECONNREFUSED) expect { send_exception(:secure => false) }.not_to raise_error end it "not fail when posting any http exception occurs" do http = stub_http Honeybadger::Sender::HTTP_ERRORS.each do |error| http.stub(:post).and_raise(error) expect { send_exception(:secure => false) }.not_to raise_error end end end context "SSL" do it "posts to the right url for non-ssl" do http = stub_http url = "http://api.honeybadger.io:80#{Honeybadger::Sender::NOTICES_URI}" uri = URI.parse(url) http.should_receive(:post).with(uri.path, anything, Honeybadger::HEADERS.merge({ 'X-API-Key' => 'abc123'})) send_exception(:secure => false) end it "post to the right path for ssl" do http = stub_http http.should_receive(:post).with(Honeybadger::Sender::NOTICES_URI, anything, Honeybadger::HEADERS.merge({ 'X-API-Key' => 'abc123'})) send_exception(:secure => true) end it "verifies the SSL peer when the use_ssl option is set to true" do url = "https://api.honeybadger.io#{Honeybadger::Sender::NOTICES_URI}" uri = URI.parse(url) real_http = Net::HTTP.new(uri.host, uri.port) real_http.stub(:post => nil) proxy = double(:new => real_http) Net::HTTP.stub(:Proxy => proxy) File.stub(:exist?).with(OpenSSL::X509::DEFAULT_CERT_FILE).and_return(false) send_exception(:secure => true) expect(real_http.use_ssl?).to be_true expect(real_http.verify_mode).to eq OpenSSL::SSL::VERIFY_PEER expect(real_http.ca_file).to eq Honeybadger.configuration.local_cert_path end it "uses the default DEFAULT_CERT_FILE if asked to" do File.should_receive(:exist?).with(OpenSSL::X509::DEFAULT_CERT_FILE).and_return(true) Honeybadger.configure do |config| config.secure = true config.use_system_ssl_cert_chain = true end sender = Honeybadger::Sender.new(Honeybadger.configuration) expect(sender.use_system_ssl_cert_chain?).to be_true http = sender.send(:setup_http_connection) expect(http.ca_file).not_to eq Honeybadger.configuration.local_cert_path end it "verifies the connection when the use_ssl option is set (VERIFY_PEER)" do sender = build_sender(:secure => true) http = sender.send(:setup_http_connection) expect(http.verify_mode).to eq OpenSSL::SSL::VERIFY_PEER end it "uses the default cert (OpenSSL::X509::DEFAULT_CERT_FILE) only if explicitly told to" do sender = build_sender(:secure => true) http = sender.send(:setup_http_connection) expect(http.ca_file).to eq Honeybadger.configuration.local_cert_path File.stub(:exist?).with(OpenSSL::X509::DEFAULT_CERT_FILE).and_return(true) sender = build_sender(:secure => true, :use_system_ssl_cert_chain => true) http = sender.send(:setup_http_connection) expect(http.ca_file).not_to eq Honeybadger.configuration.local_cert_path expect(http.ca_file).to eq OpenSSL::X509::DEFAULT_CERT_FILE end it "uses ssl if secure" do sender = build_sender(:secure => true) http = sender.send(:setup_http_connection) expect(http.port).to eq 443 end it "does not use ssl if not secure" do sender = build_sender(:secure => false) http = sender.send(:setup_http_connection) expect(http.port).to eq 80 end end context "network timeouts" do it "default the open timeout to 2 seconds" do http = stub_http http.should_receive(:open_timeout=).with(2) send_exception end it "default the read timeout to 5 seconds" do http = stub_http http.should_receive(:read_timeout=).with(5) send_exception end it "allow override of the open timeout" do http = stub_http http.should_receive(:open_timeout=).with(4) send_exception(:http_open_timeout => 4) end it "allow override of the read timeout" do http = stub_http http.should_receive(:read_timeout=).with(10) send_exception(:http_read_timeout => 10) end end def build_sender(opts = {}) Honeybadger.configure do |conf| opts.each {|opt, value| conf.send(:"#{opt}=", value) } end end def send_exception(args = {}) notice = args.delete(:notice) || build_notice_data sender = args.delete(:sender) || build_sender(args) sender.send_to_honeybadger(notice) end end