bblimke/webmock

View on GitHub
lib/webmock/http_lib_adapters/net_http.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'net/http'
require 'net/https'
require 'stringio'
require File.join(File.dirname(__FILE__), 'net_http_response')


module WebMock
  module HttpLibAdapters
    class NetHttpAdapter < HttpLibAdapter
      adapter_for :net_http

      OriginalNetHTTP = Net::HTTP unless const_defined?(:OriginalNetHTTP)
      OriginalNetBufferedIO = Net::BufferedIO unless const_defined?(:OriginalNetBufferedIO)

      def self.enable!
        Net.send(:remove_const, :BufferedIO)
        Net.send(:remove_const, :HTTP)
        Net.send(:remove_const, :HTTPSession)
        Net.send(:const_set, :HTTP, @webMockNetHTTP)
        Net.send(:const_set, :HTTPSession, @webMockNetHTTP)
        Net.send(:const_set, :BufferedIO, Net::WebMockNetBufferedIO)
      end

      def self.disable!
        Net.send(:remove_const, :BufferedIO)
        Net.send(:remove_const, :HTTP)
        Net.send(:remove_const, :HTTPSession)
        Net.send(:const_set, :HTTP, OriginalNetHTTP)
        Net.send(:const_set, :HTTPSession, OriginalNetHTTP)
        Net.send(:const_set, :BufferedIO, OriginalNetBufferedIO)

        #copy all constants from @webMockNetHTTP to original Net::HTTP
        #in case any constants were added to @webMockNetHTTP instead of Net::HTTP
        #after WebMock was enabled.
        #i.e Net::HTTP::DigestAuth
        @webMockNetHTTP.constants.each do |constant|
          if !OriginalNetHTTP.constants.map(&:to_s).include?(constant.to_s)
            OriginalNetHTTP.send(:const_set, constant, @webMockNetHTTP.const_get(constant))
          end
        end
      end

      @webMockNetHTTP = Class.new(Net::HTTP) do
        class << self
          def socket_type
            StubSocket
          end

          if Module.method(:const_defined?).arity == 1
            def const_defined?(name)
              super || self.superclass.const_defined?(name)
            end
          else
            def const_defined?(name, inherit=true)
              super || self.superclass.const_defined?(name, inherit)
            end
          end

          if Module.method(:const_get).arity != 1
            def const_get(name, inherit=true)
              super
            rescue NameError
              self.superclass.const_get(name, inherit)
            end
          end

          if Module.method(:constants).arity != 0
            def constants(inherit=true)
              (super + self.superclass.constants(inherit)).uniq
            end
          end
        end

        def request(request, body = nil, &block)
          request_signature = WebMock::NetHTTPUtility.request_signature_from_request(self, request, body)

          WebMock::RequestRegistry.instance.requested_signatures.put(request_signature)

          if webmock_response = WebMock::StubRegistry.instance.response_for_request(request_signature)
            @socket = Net::HTTP.socket_type.new
            WebMock::CallbackRegistry.invoke_callbacks(
              {lib: :net_http}, request_signature, webmock_response)
            build_net_http_response(webmock_response, &block)
          elsif WebMock.net_connect_allowed?(request_signature.uri)
            check_right_http_connection
            after_request = lambda do |response|
              if WebMock::CallbackRegistry.any_callbacks?
                webmock_response = build_webmock_response(response)
                WebMock::CallbackRegistry.invoke_callbacks(
                  {lib: :net_http, real_request: true}, request_signature, webmock_response)
              end
              response.extend Net::WebMockHTTPResponse
              block.call response if block
              response
            end
            super_with_after_request = lambda {
              response = super(request, nil, &nil)
              after_request.call(response)
            }
            if started?
              if WebMock::Config.instance.net_http_connect_on_start
                super_with_after_request.call
              else
                start_with_connect_without_finish {
                  super_with_after_request.call
                }
              end
            else
              start_with_connect {
                super_with_after_request.call
              }
            end
          else
            raise WebMock::NetConnectNotAllowedError.new(request_signature)
          end
        end

        def start_without_connect
          raise IOError, 'HTTP session already opened' if @started
          if block_given?
            begin
              @started = true
              return yield(self)
            ensure
              do_finish
            end
          end
          @started = true
          self
        end


        def start_with_connect_without_finish  # :yield: http
          if block_given?
            begin
              do_start
              return yield(self)
            end
          end
          do_start
          self
        end

        alias_method :start_with_connect, :start

        def start(&block)
          if WebMock::Config.instance.net_http_connect_on_start
            super(&block)
          else
            start_without_connect(&block)
          end
        end

        def build_net_http_response(webmock_response, &block)
          response = Net::HTTPResponse.send(:response_class, webmock_response.status[0].to_s).new("1.0", webmock_response.status[0].to_s, webmock_response.status[1])
          body = webmock_response.body
          body = nil if webmock_response.status[0].to_s == '204'

          response.instance_variable_set(:@body, body)
          webmock_response.headers.to_a.each do |name, values|
            values = [values] unless values.is_a?(Array)
            values.each do |value|
              response.add_field(name, value)
            end
          end

          response.instance_variable_set(:@read, true)

          response.extend Net::WebMockHTTPResponse

          if webmock_response.should_timeout
            raise timeout_exception, "execution expired"
          end

          webmock_response.raise_error_if_any

          yield response if block_given?

          response
        end

        def timeout_exception
          if defined?(Net::OpenTimeout)
            # Ruby 2.x
            Net::OpenTimeout
          else
            # Fallback, if things change
            Timeout::Error
          end
        end

        def build_webmock_response(net_http_response)
          webmock_response = WebMock::Response.new
          webmock_response.status = [
             net_http_response.code.to_i,
             net_http_response.message]
          webmock_response.headers = net_http_response.to_hash
          webmock_response.body = net_http_response.body
          webmock_response
        end


        def check_right_http_connection
          unless @@alredy_checked_for_right_http_connection ||= false
            WebMock::NetHTTPUtility.puts_warning_for_right_http_if_needed
            @@alredy_checked_for_right_http_connection = true
          end
        end
      end
      @webMockNetHTTP.version_1_2
      [
        [:Get, Net::HTTP::Get],
        [:Post, Net::HTTP::Post],
        [:Put, Net::HTTP::Put],
        [:Delete, Net::HTTP::Delete],
        [:Head, Net::HTTP::Head],
        [:Options, Net::HTTP::Options]
      ].each do |c|
        @webMockNetHTTP.const_set(c[0], c[1])
      end
    end
  end
end

# patch for StringIO behavior in Ruby 2.2.3
# https://github.com/bblimke/webmock/issues/558
class PatchedStringIO < StringIO #:nodoc:

  alias_method :orig_read_nonblock, :read_nonblock

  def read_nonblock(size, *args, **kwargs)
    args.reject! {|arg| !arg.is_a?(Hash)}
    orig_read_nonblock(size, *args, **kwargs)
  end

end

class StubSocket #:nodoc:

  attr_accessor :read_timeout, :continue_timeout, :write_timeout

  def initialize(*args)
  end

  def closed?
    @closed ||= true
  end

  def close
  end

  def readuntil(*args)
  end

  def io
    @io ||= StubIO.new
  end

  class StubIO
    def setsockopt(*args); end
  end
end

module Net  #:nodoc: all

  class WebMockNetBufferedIO < BufferedIO
    def initialize(io, *args, **kwargs)
      io = case io
      when Socket, OpenSSL::SSL::SSLSocket, IO
        io
      when StringIO
        PatchedStringIO.new(io.string)
      when String
        PatchedStringIO.new(io)
      end
      raise "Unable to create local socket" unless io

      # Prior to 2.4.0 `BufferedIO` only takes a single argument (`io`) with no
      # options. Here we pass through our full set of arguments only if we're
      # on 2.4.0 or later, and use a simplified invocation otherwise.
      if RUBY_VERSION >= '2.4.0'
        super
      else
        super(io)
      end
    end

    if RUBY_VERSION >= '2.6.0'
      # https://github.com/ruby/ruby/blob/7d02441f0d6e5c9d0a73a024519eba4f69e36dce/lib/net/protocol.rb#L208
      # Modified version of method from ruby, so that nil is always passed into orig_read_nonblock to avoid timeout
      def rbuf_fill
        case rv = @io.read_nonblock(BUFSIZE, nil, exception: false)
        when String
          return if rv.nil?
          @rbuf << rv
          rv.clear
          return
        when :wait_readable
          @io.to_io.wait_readable(@read_timeout) or raise Net::ReadTimeout
        when :wait_writable
          @io.to_io.wait_writable(@read_timeout) or raise Net::ReadTimeout
        when nil
          raise EOFError, 'end of file reached'
        end while true
      end
    end
  end

end


module WebMock
  module NetHTTPUtility

    def self.request_signature_from_request(net_http, request, body = nil)
      path = request.path

      if path.respond_to?(:request_uri) #https://github.com/bblimke/webmock/issues/288
        path = path.request_uri
      end

      path = WebMock::Util::URI.heuristic_parse(path).request_uri if path =~ /^http/

      uri = get_uri(net_http, path)
      method = request.method.downcase.to_sym

      headers = Hash[*request.to_hash.map {|k,v| [k, v]}.inject([]) {|r,x| r + x}]
      validate_headers(headers)

      if request.body_stream
        body = request.body_stream.read
        request.body_stream = nil
      end

      if body != nil && body.respond_to?(:read)
        request.set_body_internal body.read
      else
        request.set_body_internal body
      end

      WebMock::RequestSignature.new(method, uri, body: request.body, headers: headers)
    end

    def self.get_uri(net_http, path)
      protocol = net_http.use_ssl? ? "https" : "http"

      hostname = net_http.address
      hostname = "[#{hostname}]" if /\A\[.*\]\z/ !~ hostname && /:/ =~ hostname

      "#{protocol}://#{hostname}:#{net_http.port}#{path}"
    end

    def self.validate_headers(headers)
      # For Ruby versions < 2.3.0, if you make a request with headers that are symbols
      # Net::HTTP raises a NoMethodError
      #
      # WebMock normalizes headers when creating a RequestSignature,
      # and will update all headers from symbols to strings.
      #
      # This could create a false positive in a test suite with WebMock.
      #
      # So before this point, WebMock raises an ArgumentError if any of the headers are symbols
      # instead of the cryptic NoMethodError "undefined method `split' ...` from Net::HTTP
      if Gem::Version.new(RUBY_VERSION.dup) < Gem::Version.new('2.3.0')
        header_as_symbol = headers.keys.find {|header| header.is_a? Symbol}
        if header_as_symbol
          raise ArgumentError.new("Net:HTTP does not accept headers as symbols")
        end
      end
    end

    def self.check_right_http_connection
      @was_right_http_connection_loaded = defined?(RightHttpConnection)
    end

    def self.puts_warning_for_right_http_if_needed
      if !@was_right_http_connection_loaded && defined?(RightHttpConnection)
        $stderr.puts "\nWarning: RightHttpConnection has to be required before WebMock is required !!!\n"
      end
    end

  end
end

WebMock::NetHTTPUtility.check_right_http_connection