Back to Repositories

Testing HTTP Connection Adapter Implementation in HTTParty

This test suite thoroughly validates the HTTParty::ConnectionAdapter class, focusing on HTTP/HTTPS connection handling, SSL certificate management, and timeout configurations. The tests ensure proper URI handling, connection settings, and secure communication setup.

Test Coverage Overview

The test suite provides comprehensive coverage of connection adapter functionality including:
  • URI validation and initialization
  • SSL/TLS configuration and certificate handling
  • Timeout settings (read, write, open)
  • Proxy configuration
  • IPv6 support
  • Port handling for HTTP/HTTPS

Implementation Analysis

The testing approach uses RSpec’s describe/context blocks to organize test scenarios logically. It leverages doubles and mocks extensively to isolate connection adapter behavior from actual HTTP connections.

Key patterns include shared examples for SSL verification, thorough edge case handling for timeout configurations, and comprehensive certificate management testing.

Technical Details

Testing tools and configuration:
  • RSpec for test framework
  • Mock objects for Net::HTTP isolation
  • OpenSSL integration for certificate testing
  • Custom matchers for SSL verification
  • Multiple context blocks for different scenarios

Best Practices Demonstrated

The test suite exemplifies several testing best practices:
  • Comprehensive error case handling
  • Isolated unit testing with mocks
  • Clear test organization and naming
  • Thorough validation of security features
  • Explicit testing of default behaviors and overrides

jnunemaker/httparty

spec/httparty/connection_adapter_spec.rb

            
require 'spec_helper'

RSpec.describe HTTParty::ConnectionAdapter do
  describe "initialization" do
    let(:uri) { URI 'http://www.google.com' }
    it "takes a URI as input" do
      HTTParty::ConnectionAdapter.new(uri)
    end

    it "raises an ArgumentError if the uri is nil" do
      expect { HTTParty::ConnectionAdapter.new(nil) }.to raise_error ArgumentError
    end

    it "raises an ArgumentError if the uri is a String" do
      expect { HTTParty::ConnectionAdapter.new('http://www.google.com') }.to raise_error ArgumentError
    end

    it "sets the uri" do
      adapter = HTTParty::ConnectionAdapter.new(uri)
      expect(adapter.uri).to be uri
    end

    it "also accepts an optional options hash" do
      HTTParty::ConnectionAdapter.new(uri, {})
    end

    it "sets the options" do
      options = {foo: :bar}
      adapter = HTTParty::ConnectionAdapter.new(uri, options)
      expect(adapter.options.keys).to include(:verify, :verify_peer, :foo)
    end
  end

  describe ".call" do
    let(:uri) { URI 'http://www.google.com' }
    let(:options) { { foo: :bar } }

    it "generates an HTTParty::ConnectionAdapter instance with the given uri and options" do
      expect(HTTParty::ConnectionAdapter).to receive(:new).with(uri, options).and_return(double(connection: nil))
      HTTParty::ConnectionAdapter.call(uri, options)
    end

    it "calls #connection on the connection adapter" do
      adapter = double('Adapter')
      connection = double('Connection')
      expect(adapter).to receive(:connection).and_return(connection)
      allow(HTTParty::ConnectionAdapter).to receive_messages(new: adapter)
      expect(HTTParty::ConnectionAdapter.call(uri, options)).to be connection
    end
  end

  describe '#connection' do
    let(:uri) { URI 'http://www.google.com' }
    let(:options) { Hash.new }
    let(:adapter) { HTTParty::ConnectionAdapter.new(uri, options) }

    describe "the resulting connection" do
      subject { adapter.connection }
      it { is_expected.to be_an_instance_of Net::HTTP }

      context "using port 80" do
        let(:uri) { URI 'http://foobar.com' }
        it { is_expected.not_to use_ssl }
      end

      context "when dealing with ssl" do
        let(:uri) { URI 'https://foobar.com' }

        context "uses the system cert_store, by default" do
          let!(:system_cert_store) do
            system_cert_store = double('default_cert_store')
            expect(system_cert_store).to receive(:set_default_paths)
            expect(OpenSSL::X509::Store).to receive(:new).and_return(system_cert_store)
            system_cert_store
          end
          it { is_expected.to use_cert_store(system_cert_store) }
        end

        context "should use the specified cert store, when one is given" do
          let(:custom_cert_store) { double('custom_cert_store') }
          let(:options) { {cert_store: custom_cert_store} }
          it { is_expected.to use_cert_store(custom_cert_store) }
        end

        context "using port 443 for ssl" do
          let(:uri) { URI 'https://api.foo.com/v1:443' }
          it { is_expected.to use_ssl }
        end

        context "https scheme with default port" do
          it { is_expected.to use_ssl }
        end

        context "https scheme with non-standard port" do
          let(:uri) { URI 'https://foobar.com:123456' }
          it { is_expected.to use_ssl }
        end

        context "when ssl version is set" do
          let(:options) { {ssl_version: :TLSv1} }

          it "sets ssl version" do
            expect(subject.ssl_version).to eq(:TLSv1)
          end
        end
      end

      context "when dealing with IPv6" do
        let(:uri) { URI 'http://[fd00::1]' }

        it "strips brackets from the address" do
          expect(subject.address).to eq('fd00::1')
        end
      end

      context "specifying ciphers" do
        let(:options) { {ciphers: 'RC4-SHA' } }

        it "should set the ciphers on the connection" do
          expect(subject.ciphers).to eq('RC4-SHA')
        end
      end

      context "when timeout is not set" do
        it "doesn't set the timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false
          )
          expect(http).not_to receive(:open_timeout=)
          expect(http).not_to receive(:read_timeout=)
          expect(http).not_to receive(:write_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)

          adapter.connection
        end
      end

      context "when setting timeout" do
        context "to 5 seconds" do
          let(:options) { {timeout: 5} }

          describe '#open_timeout' do
            subject { super().open_timeout }
            it { is_expected.to eq(5) }
          end

          describe '#read_timeout' do
            subject { super().read_timeout }
            it { is_expected.to eq(5) }
          end

          describe '#write_timeout' do
            subject { super().write_timeout }
            it { is_expected.to eq(5) }
          end
        end

        context "and timeout is a string" do
          let(:options) { {timeout: "five seconds"} }

          it "doesn't set the timeout" do
            http = double(
              "http",
              :null_object => true,
              :use_ssl= => false,
              :use_ssl? => false
            )
            expect(http).not_to receive(:open_timeout=)
            expect(http).not_to receive(:read_timeout=)
            expect(http).not_to receive(:write_timeout=)
            allow(Net::HTTP).to receive_messages(new: http)

            adapter.connection
          end
        end
      end

      context "when timeout is not set and read_timeout is set to 6 seconds" do
        let(:options) { {read_timeout: 6} }

        describe '#read_timeout' do
          subject { super().read_timeout }
          it { is_expected.to eq(6) }
        end

        it "should not set the open_timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :read_timeout= => 0
          )
          expect(http).not_to receive(:open_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end

        it "should not set the write_timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :read_timeout= => 0
          )
          expect(http).not_to receive(:write_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end
      end

      context "when timeout is set and read_timeout is set to 6 seconds" do
        let(:options) { {timeout: 5, read_timeout: 6} }

        describe '#open_timeout' do
          subject { super().open_timeout }
          it { is_expected.to eq(5) }
        end

        describe '#write_timeout' do
          subject { super().write_timeout }
          it { is_expected.to eq(5) }
        end

        describe '#read_timeout' do
          subject { super().read_timeout }
          it { is_expected.to eq(6) }
        end

        it "should override the timeout option" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :read_timeout= => 0,
            :open_timeout= => 0,
            :write_timeout= => 0,
          )
          expect(http).to receive(:open_timeout=)
          expect(http).to receive(:read_timeout=).twice
          expect(http).to receive(:write_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end
      end

      context "when timeout is not set and open_timeout is set to 7 seconds" do
        let(:options) { {open_timeout: 7} }

        describe '#open_timeout' do
          subject { super().open_timeout }
          it { is_expected.to eq(7) }
        end

        it "should not set the read_timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :open_timeout= => 0
          )
          expect(http).not_to receive(:read_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end

        it "should not set the write_timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :open_timeout= => 0
          )
          expect(http).not_to receive(:write_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end
      end

      context "when timeout is set and open_timeout is set to 7 seconds" do
        let(:options) { {timeout: 5, open_timeout: 7} }

        describe '#open_timeout' do
          subject { super().open_timeout }
          it { is_expected.to eq(7) }
        end

        describe '#write_timeout' do
          subject { super().write_timeout }
          it { is_expected.to eq(5) }
        end

        describe '#read_timeout' do
          subject { super().read_timeout }
          it { is_expected.to eq(5) }
        end

        it "should override the timeout option" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :read_timeout= => 0,
            :open_timeout= => 0,
            :write_timeout= => 0,
          )
          expect(http).to receive(:open_timeout=).twice
          expect(http).to receive(:read_timeout=)
          expect(http).to receive(:write_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end
      end

      context "when timeout is not set and write_timeout is set to 8 seconds" do
        let(:options) { {write_timeout: 8} }

        describe '#write_timeout' do
          subject { super().write_timeout }
          it { is_expected.to eq(8) }
        end

        it "should not set the open timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :read_timeout= => 0,
            :open_timeout= => 0,
            :write_timeout= => 0,

          )
          expect(http).not_to receive(:open_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end

        it "should not set the read timeout" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false,
            :read_timeout= => 0,
            :open_timeout= => 0,
            :write_timeout= => 0,

          )
          expect(http).not_to receive(:read_timeout=)
          allow(Net::HTTP).to receive_messages(new: http)
          adapter.connection
        end

        context "when timeout is set and write_timeout is set to 8 seconds" do
          let(:options) { {timeout: 2, write_timeout: 8} }

          describe '#write_timeout' do
            subject { super().write_timeout }
            it { is_expected.to eq(8) }
          end

          it "should override the timeout option" do
            http = double(
              "http",
              :null_object => true,
              :use_ssl= => false,
              :use_ssl? => false,
              :read_timeout= => 0,
              :open_timeout= => 0,
              :write_timeout= => 0,
            )
            expect(http).to receive(:read_timeout=)
            expect(http).to receive(:open_timeout=)
            expect(http).to receive(:write_timeout=).twice
            allow(Net::HTTP).to receive_messages(new: http)
            adapter.connection
          end
        end
      end

      context "when max_retries is not set" do
        it "doesn't set the max_retries" do
          http = double(
            "http",
            :null_object => true,
            :use_ssl= => false,
            :use_ssl? => false
          )
          expect(http).not_to receive(:max_retries=)
          allow(Net::HTTP).to receive_messages(new: http)

          adapter.connection
        end
      end

      context "when setting max_retries" do
        context "to 5 times" do
          let(:options) { {max_retries: 5} }
          describe '#max_retries' do
            subject { super().max_retries }
            it { is_expected.to eq(5) }
          end
        end

        context "to 0 times" do
          let(:options) { {max_retries: 0} }
          describe '#max_retries' do
            subject { super().max_retries }
            it { is_expected.to eq(0) }
          end
        end

        context "and max_retries is a string" do
          let(:options) { {max_retries: "five times"} }

          it "doesn't set the max_retries" do
            http = double(
              "http",
              :null_object => true,
              :use_ssl= => false,
              :use_ssl? => false
            )
            expect(http).not_to receive(:max_retries=)
            allow(Net::HTTP).to receive_messages(new: http)

            adapter.connection
          end
        end
      end

      context "when debug_output" do
        let(:http) { Net::HTTP.new(uri) }
        before do
          allow(Net::HTTP).to receive_messages(new: http)
        end

        context "is set to $stderr" do
          let(:options) { {debug_output: $stderr} }
          it "has debug output set" do
            expect(http).to receive(:set_debug_output).with($stderr)
            adapter.connection
          end
        end

        context "is not provided" do
          it "does not set_debug_output" do
            expect(http).not_to receive(:set_debug_output)
            adapter.connection
          end
        end
      end

      context 'when providing proxy address and port' do
        let(:options) { {http_proxyaddr: '1.2.3.4', http_proxyport: 8080} }

        it { is_expected.to be_a_proxy }

        describe '#proxy_address' do
          subject { super().proxy_address }
          it { is_expected.to eq('1.2.3.4') }
        end

        describe '#proxy_port' do
          subject { super().proxy_port }
          it { is_expected.to eq(8080) }
        end

        context 'as well as proxy user and password' do
          let(:options) do
            {
              http_proxyaddr: '1.2.3.4',
              http_proxyport: 8080,
              http_proxyuser: 'user',
              http_proxypass: 'pass'
            }
          end

          describe '#proxy_user' do
            subject { super().proxy_user }
            it { is_expected.to eq('user') }
          end

          describe '#proxy_pass' do
            subject { super().proxy_pass }
            it { is_expected.to eq('pass') }
          end
        end
      end

      context 'when providing nil as proxy address' do
        let(:uri) { URI 'http://noproxytest.com' }
        let(:options) { {http_proxyaddr: nil} }

        it { is_expected.not_to be_a_proxy }

        it "does pass nil proxy parameters to the connection, this forces to not use a proxy" do
          http = Net::HTTP.new("noproxytest.com")
          expect(Net::HTTP).to receive(:new).once.with("noproxytest.com", 80, nil, nil, nil, nil).and_return(http)
          adapter.connection
        end
      end

      context 'when not providing a proxy address' do
        let(:uri) { URI 'http://proxytest.com' }

        it "does not pass any proxy parameters to the connection" do
          http = Net::HTTP.new("proxytest.com")
          expect(Net::HTTP).to receive(:new).once.with("proxytest.com", 80).and_return(http)
          adapter.connection
        end
      end

      context 'when providing a local bind address and port' do
        let(:options) { {local_host: "127.0.0.1", local_port: 12345 } }

        describe '#local_host' do
          subject { super().local_host }
          it { is_expected.to eq('127.0.0.1') }
        end

        describe '#local_port' do
          subject { super().local_port }
          it { is_expected.to eq(12345) }
        end
      end

      context "when providing PEM certificates" do
        let(:pem) { :pem_contents }
        let(:options) { {pem: pem, pem_password: "password"} }

        context "when scheme is https" do
          let(:uri) { URI 'https://google.com' }
          let(:cert) { double("OpenSSL::X509::Certificate") }
          let(:key) { double("OpenSSL::PKey::RSA") }

          before do
            expect(OpenSSL::X509::Certificate).to receive(:new).with(pem).and_return(cert)
            expect(OpenSSL::PKey).to receive(:read).with(pem, "password").and_return(key)
          end

          it "uses the provided PEM certificate" do
            expect(subject.cert).to eq(cert)
            expect(subject.key).to eq(key)
          end

          it "will verify the certificate" do
            expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
          end

          context "when options include verify=false" do
            let(:options) { {pem: pem, pem_password: "password", verify: false} }

            it "should not verify the certificate" do
              expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE)
            end
          end
          context "when options include verify_peer=false" do
            let(:options) { {pem: pem, pem_password: "password", verify_peer: false} }

            it "should not verify the certificate" do
              expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE)
            end
          end
        end

        context "when scheme is not https" do
          let(:uri) { URI 'http://google.com' }
          let(:http) { Net::HTTP.new(uri) }

          before do
            allow(Net::HTTP).to receive_messages(new: http)
            expect(OpenSSL::X509::Certificate).not_to receive(:new).with(pem)
            expect(OpenSSL::PKey::RSA).not_to receive(:new).with(pem, "password")
            expect(http).not_to receive(:cert=)
            expect(http).not_to receive(:key=)
          end

          it "has no PEM certificate " do
            expect(subject.cert).to be_nil
            expect(subject.key).to be_nil
          end
        end
      end

      context "when providing PKCS12 certificates" do
        let(:p12) { :p12_contents }
        let(:options) { {p12: p12, p12_password: "password"} }

        context "when scheme is https" do
          let(:uri) { URI 'https://google.com' }
          let(:pkcs12) { double("OpenSSL::PKCS12", certificate: cert, key: key) }
          let(:cert) { double("OpenSSL::X509::Certificate") }
          let(:key) { double("OpenSSL::PKey::RSA") }

          before do
            expect(OpenSSL::PKCS12).to receive(:new).with(p12, "password").and_return(pkcs12)
          end

          it "uses the provided P12 certificate " do
            expect(subject.cert).to eq(cert)
            expect(subject.key).to eq(key)
          end

          it "will verify the certificate" do
            expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_PEER)
          end

          context "when options include verify=false" do
            let(:options) { {p12: p12, p12_password: "password", verify: false} }

            it "should not verify the certificate" do
              expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE)
            end
          end
          context "when options include verify_peer=false" do
            let(:options) { {p12: p12, p12_password: "password", verify_peer: false} }

            it "should not verify the certificate" do
              expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE)
            end
          end
        end

        context "when scheme is not https" do
          let(:uri) { URI 'http://google.com' }
          let(:http) { Net::HTTP.new(uri) }

          before do
            allow(Net::HTTP).to receive_messages(new: http)
            expect(OpenSSL::PKCS12).not_to receive(:new).with(p12, "password")
            expect(http).not_to receive(:cert=)
            expect(http).not_to receive(:key=)
          end

          it "has no PKCS12 certificate " do
            expect(subject.cert).to be_nil
            expect(subject.key).to be_nil
          end
        end
      end

      context "when uri port is not defined" do
        context "falls back to 80 port on http" do
          let(:uri) { URI 'http://foobar.com' }
          before { allow(uri).to receive(:port).and_return(nil) }
          it { expect(subject.port).to be 80 }
        end

        context "falls back to 443 port on https" do
          let(:uri) { URI 'https://foobar.com' }
          before { allow(uri).to receive(:port).and_return(nil) }
          it { expect(subject.port).to be 443 }
        end
      end
    end
  end
end